├── .gitignore ├── LICENSE ├── QmlPlotting.qbs ├── README.md ├── doc ├── demo-colormappedimage.png ├── demo-containers.png └── demo-plotxy.png ├── examples ├── demo │ ├── ColormappedImage.qml │ ├── Containers.qml │ ├── XYPlot.qml │ ├── demo.cpp │ ├── demo.qbs │ ├── demo.qml │ ├── demo.qrc │ └── qtquickcontrols2.conf └── examples.qbs ├── src ├── QmlPlotting.qbs ├── plugin │ └── plugin.cpp ├── qmlplotting │ ├── colormappedimage.cpp │ ├── colormappedimage.h │ ├── colormaps.h │ ├── dataclient.cpp │ ├── dataclient.h │ ├── datasource.cpp │ ├── datasource.h │ ├── plotgroup.cpp │ ├── plotgroup.h │ ├── qsgdatatexture.cpp │ ├── qsgdatatexture.h │ ├── sliceplot.cpp │ ├── sliceplot.h │ ├── xyplot.cpp │ └── xyplot.h └── ressources │ ├── Axes.qml │ ├── Container.qml │ ├── MovableContainer.qml │ ├── Scale.qml │ ├── Utils.js │ ├── ZoomPanTool.qml │ └── qmldir └── tests ├── auto ├── tst_basic.cpp ├── tst_basic.qbs └── tst_basic.qml └── tests.qbs /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | moc_*.cpp 3 | Makefile 4 | *.qmlc 5 | *.so 6 | *.user 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Peter Würtz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /QmlPlotting.qbs: -------------------------------------------------------------------------------- 1 | import qbs 2 | 3 | Project { 4 | references: [ 5 | "src/QmlPlotting.qbs", 6 | "tests/tests.qbs", 7 | "examples/examples.qbs", 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QmlPlotting 2 | > **NOTE:** This project is end-of-life. A follow-up based on Qt6 is being developed at [QtQuickPlotScene](https://github.com/pwuertz/QtQuickPlotScene). 3 | 4 | QmlPlotting is a collection of QtQuick items for scientific data visualization bundled as QtQuick plugin. 5 | 6 | ![XY Plot](doc/demo-plotxy.png) 7 | ![2D Data](doc/demo-colormappedimage.png) 8 | ![Containers](doc/demo-containers.png) 9 | 10 | ## Building 11 | The QmlPlotting project and build process is based on [QBS](http://doc.qt.io/qbs/). The easiest way for building the plugin and running the examples is to open and build the project with a recent version of QtCreator. The minimum requirements for building QmlPlotting are [Qt 5.9](https://www.qt.io/download/) (or later) and a compiler supporting C++14. 12 | 13 | ## Documentation 14 | A documentation of the API does not exist yet. Until then the example application serves as a reference for using QmlPlotting in custom applications. 15 | 16 | ## License 17 | QmlPlotting is available under the [MIT license](LICENSE). 18 | -------------------------------------------------------------------------------- /doc/demo-colormappedimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwuertz/qmlplotting/90c7e0a3ef84b66f22607f0330e71bd71fa4844e/doc/demo-colormappedimage.png -------------------------------------------------------------------------------- /doc/demo-containers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwuertz/qmlplotting/90c7e0a3ef84b66f22607f0330e71bd71fa4844e/doc/demo-containers.png -------------------------------------------------------------------------------- /doc/demo-plotxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwuertz/qmlplotting/90c7e0a3ef84b66f22607f0330e71bd71fa4844e/doc/demo-plotxy.png -------------------------------------------------------------------------------- /examples/demo/ColormappedImage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.7 2 | import QtQuick.Controls 2.2 3 | import QtQuick.Layouts 1.2 4 | import QmlPlotting 2.0 as QmlPlotting 5 | 6 | /** 7 | ColormappedImage example 8 | */ 9 | 10 | Page { 11 | id: root 12 | 13 | // QmlPlotting colormap image with default test data 14 | QmlPlotting.ColormappedImage { 15 | id: image 16 | anchors.fill: parent 17 | colormap: colorMaps.get(0).name 18 | minimumValue: rangeSlider.first.value 19 | maximumValue: rangeSlider.second.value 20 | dataSource: QmlPlotting.DataSource {} 21 | Component.onCompleted: dataSource.setTestData2D(); 22 | } 23 | 24 | // Image value range editing 25 | Pane { 26 | anchors.fill: rangeGroup 27 | anchors.margins: -Qt.application.font.pixelSize / 2 28 | opacity: 0.8 29 | } 30 | ColumnLayout { 31 | id: rangeGroup 32 | anchors.left: parent.left 33 | anchors.bottom: parent.bottom 34 | anchors.margins: Qt.application.font.pixelSize 35 | Label { 36 | readonly property string value1: rangeSlider.first.value.toFixed(2) 37 | readonly property string value2: rangeSlider.second.value.toFixed(2) 38 | text: "Value range " + value1 + " to " + value2 39 | } 40 | RangeSlider { 41 | id: rangeSlider 42 | from: -1 43 | to: 2 44 | first.value: 0. 45 | second.value: 1. 46 | implicitWidth: .4 * root.width 47 | } 48 | } 49 | 50 | // Colormap selection 51 | ListModel { 52 | id: colorMaps 53 | ListElement { name: "viridis" } 54 | ListElement { name: "bwr" } 55 | ListElement { name: "ferrugineus" } 56 | ListElement { name: "turbo" } 57 | ListElement { name: "jet" } 58 | ListElement { name: "wjet" } 59 | ListElement { name: "hot" } 60 | ListElement { name: "gray" } 61 | } 62 | Pane { 63 | anchors.fill: colorGroup 64 | anchors.margins: -Qt.application.font.pixelSize / 2 65 | opacity: 0.8 66 | } 67 | ColumnLayout { 68 | id: colorGroup 69 | anchors.right: parent.right 70 | anchors.verticalCenter: parent.verticalCenter 71 | anchors.margins: Qt.application.font.pixelSize 72 | Label { 73 | Layout.alignment: Qt.AlignHCenter 74 | text: "Colormaps" 75 | } 76 | Repeater { 77 | model: colorMaps 78 | Button { 79 | Layout.alignment: Qt.AlignHCenter 80 | text: model.name 81 | onClicked: { image.colormap = model.name; } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/demo/Containers.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.7 2 | import QtQuick.Controls 2.2 3 | import QtQuick.Layouts 1.2 4 | import QmlPlotting 2.0 as QmlPlotting 5 | 6 | /** 7 | Container / MovableContainer example 8 | */ 9 | 10 | Page { 11 | id: root 12 | // Plot axes for showing container positions 13 | QmlPlotting.Axes { 14 | anchors.fill: parent 15 | // Define plot group containing zoom/pan tool and example containers with Quick items 16 | plotGroup: QmlPlotting.PlotGroup { 17 | clip: true 18 | viewRect: Qt.rect(-3, -2, 6, 5) 19 | viewMode: QmlPlotting.PlotGroup.PreserveAspectFit 20 | plotItems: [ 21 | QmlPlotting.ZoomPanTool {}, 22 | // Fixed container 23 | QmlPlotting.Container { 24 | itemRect: Qt.rect(-2, 0, 1, 1) 25 | Rectangle { 26 | color: "lightsteelblue"; border.width: 1; border.color: "steelblue" 27 | Label { text: "Fixed"; anchors.centerIn: parent } 28 | } 29 | }, 30 | // Resize- and movable container 31 | QmlPlotting.MovableContainer { 32 | itemRect: Qt.rect(0, 0, 1, 1) 33 | onItemRectChanged: plotLabel.showRect(itemRect) 34 | Rectangle { 35 | color: "lightsteelblue"; border.width: 1; border.color: "steelblue" 36 | Label { text: "Movable"; anchors.centerIn: parent } 37 | } 38 | }, 39 | // Container with resize constraint 40 | QmlPlotting.MovableContainer { 41 | itemRect: Qt.rect(2, 0, 1, 1) 42 | maxItemRect: Qt.rect(-3, -2, 6, 5) 43 | onItemRectChanged: plotLabel.showRect(itemRect) 44 | aspectFixed: true 45 | Rectangle { 46 | color: "lightsteelblue"; border.width: 1; border.color: "steelblue" 47 | radius: width 48 | Label { text: "Constrained"; anchors.centerIn: parent } 49 | } 50 | } 51 | ] 52 | 53 | // Add label for showing rectangle coordinates 54 | Label { 55 | id: plotLabel 56 | text: "Move a container" 57 | anchors.top: parent.top 58 | anchors.right: parent.right 59 | anchors.margins: Qt.application.font.pixelSize 60 | Behavior on opacity { NumberAnimation { duration: 200 } } 61 | function showRect(rect) { 62 | var coords = "(" + rect.x.toFixed(2) + ", " + rect.y.toFixed(2) + ")"; 63 | var size = rect.width.toFixed(2) + " x " + rect.height.toFixed(2) 64 | text = "Bottom left " + coords + ", Size " + size; 65 | opacity = 1; 66 | labelTimeout.restart(); 67 | } 68 | Timer { 69 | id: labelTimeout 70 | interval: 2000 71 | running: false 72 | repeat: false 73 | onTriggered: plotLabel.opacity = 0; 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/demo/XYPlot.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.7 2 | import QtQuick.Controls 2.2 3 | import QtQuick.Layouts 1.2 4 | import QmlPlotting 2.0 as QmlPlotting 5 | 6 | /** 7 | XYPlot example 8 | */ 9 | 10 | Page { 11 | id: root 12 | 13 | ColumnLayout { 14 | anchors.fill: parent 15 | anchors.rightMargin: 5 16 | // Plot axes for XY plot demonstration 17 | QmlPlotting.Axes { 18 | Layout.fillHeight: true 19 | Layout.fillWidth: true 20 | xLabel: "X Axis" 21 | yLabel: "Y Axis" 22 | // Define plot group containing zoom/pan tool and XY plot item 23 | plotGroup: QmlPlotting.PlotGroup { 24 | viewRect: Qt.rect(-1, -.1, 2, 1.2) 25 | plotItems: [ 26 | QmlPlotting.ZoomPanTool { 27 | maxViewRect: Qt.rect(-2., -1., 4., 3.) 28 | minimumSize: Qt.size(0.1, 0.1) 29 | }, 30 | // XY plot item with default test data 31 | QmlPlotting.XYPlot { 32 | id: xyPlot 33 | dataSource: QmlPlotting.DataSource {} 34 | Component.onCompleted: dataSource.setTestData1D(); 35 | lineColor: Qt.hsva(hueSlider.hue, .7, .7, .7) 36 | lineWidth: lineWidthSlider.value 37 | markerColor: Qt.hsva(hueSlider.hue, .7, .7, .6) 38 | markerSize: markerSizeSlider.value 39 | } 40 | ] 41 | } 42 | } 43 | // Controls for changing plot properties 44 | RowLayout { 45 | Layout.margins: Qt.application.font.pixelSize 46 | spacing: Qt.application.font.pixelSize / 2 47 | // Change hue values 48 | Slider { 49 | id: hueSlider 50 | readonly property real hue: value * (1. / to) 51 | from: 0; to: 100; stepSize: 1; value: 55 52 | ToolTip.visible: hovered || pressed 53 | ToolTip.text: "Hue value" 54 | } 55 | // Change line width 56 | Slider { 57 | id: lineWidthSlider 58 | from: 1; to: 10; stepSize: 1; value: 3 59 | ToolTip.visible: hovered || pressed 60 | ToolTip.text: "Line width " + value 61 | } 62 | // Change marker size 63 | Slider { 64 | id: markerSizeSlider 65 | from: 1; to: 50; stepSize: 1; value: 15 66 | ToolTip.visible: hovered || pressed 67 | ToolTip.text: "Marker size " + value 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/demo/demo.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 8 | QGuiApplication app(argc, argv); 9 | QQmlApplicationEngine engine; 10 | engine.addImportPath(QMLPLOTTING_IMPORT_DIR); 11 | qInfo() << "Adding QML import path:" << QStringLiteral(QMLPLOTTING_IMPORT_DIR); 12 | 13 | const QUrl mainQml(QStringLiteral("qrc:/demo.qml")); 14 | // Catch the objectCreated signal, so that we can determine if the root component was loaded 15 | // successfully. If not, then the object created from it will be null. The root component may 16 | // get loaded asynchronously. 17 | const QMetaObject::Connection connection = QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app, [&](QObject *object, const QUrl &url) { 18 | if (url != mainQml) { 19 | return; 20 | } 21 | if (object == nullptr) { 22 | app.exit(-1); 23 | } else { 24 | QObject::disconnect(connection); 25 | } 26 | }, Qt::QueuedConnection); 27 | engine.load(mainQml); 28 | return app.exec(); 29 | } 30 | -------------------------------------------------------------------------------- /examples/demo/demo.qbs: -------------------------------------------------------------------------------- 1 | import qbs 2 | import qbs.FileInfo 3 | 4 | CppApplication { 5 | Depends { name: "Qt"; submodules: [ "core", "quick" ] } 6 | Depends { name: "qmlplotting"} 7 | cpp.cxxLanguageVersion: "c++14" 8 | 9 | property path qmlplottingImportDir: FileInfo.joinPaths(project.buildDirectory, "install-root") 10 | property pathList qmlImportPaths: [ qmlplottingImportDir ] 11 | cpp.defines: ["QMLPLOTTING_IMPORT_DIR=\"" + FileInfo.fromWindowsSeparators(qmlplottingImportDir) + "\""] 12 | 13 | files: [ 14 | "demo.cpp", 15 | "demo.qrc", 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /examples/demo/demo.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.7 2 | import QtQuick.Controls 2.2 3 | import QtQuick.Layouts 1.2 4 | import QmlPlotting 2.0 as QmlPlotting 5 | 6 | ApplicationWindow { 7 | id: approot 8 | visible: true 9 | title: "QmlPlotting Demo" 10 | minimumWidth: 800 11 | minimumHeight: 600 12 | 13 | // Demonstration pages 14 | ListModel { 15 | id: demoPages 16 | ListElement { name: "2D Data"; source: "qrc:/pages/ColormappedImage.qml" } 17 | ListElement { name: "XY Plot"; source: "qrc:/pages/XYPlot.qml"} 18 | ListElement { name: "Containers"; source: "qrc:/pages/Containers.qml"} 19 | } 20 | header: ToolBar { 21 | focus: true 22 | Keys.onEscapePressed: { approot.close(); } 23 | RowLayout { 24 | anchors.horizontalCenter: parent.horizontalCenter 25 | spacing: Qt.application.font.pixelSize 26 | Label { text: approot.title } 27 | ComboBox { 28 | id: pageSelector 29 | textRole: "name" 30 | model: demoPages 31 | currentIndex: Qt.application.arguments.length > 1 ? Qt.application.arguments[1] : 0 32 | implicitWidth: 10 * Qt.application.font.pixelSize 33 | } 34 | } 35 | } 36 | Loader { 37 | anchors.fill: parent 38 | source: demoPages.get(pageSelector.currentIndex).source 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/demo/demo.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | demo.qml 4 | qtquickcontrols2.conf 5 | 6 | 7 | ColormappedImage.qml 8 | XYPlot.qml 9 | Containers.qml 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/demo/qtquickcontrols2.conf: -------------------------------------------------------------------------------- 1 | [Controls] 2 | Style=Material 3 | -------------------------------------------------------------------------------- /examples/examples.qbs: -------------------------------------------------------------------------------- 1 | import qbs 2 | 3 | Project { 4 | references: [ 5 | "demo/demo.qbs", 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/QmlPlotting.qbs: -------------------------------------------------------------------------------- 1 | import qbs 2 | import qbs.FileInfo 3 | 4 | LoadableModule { 5 | Depends { name: "Qt.quick" } 6 | name: "qmlplotting" 7 | targetName: "qmlplottingplugin" 8 | cpp.cxxLanguageVersion: "c++14" 9 | 10 | Group { 11 | name: "Plugin" 12 | files: [ 13 | "plugin/plugin.cpp", 14 | "qmlplotting/*.cpp", 15 | "qmlplotting/*.h", 16 | ] 17 | } 18 | 19 | Group { 20 | name: "Ressources" 21 | files: [ 22 | "ressources/qmldir", 23 | "ressources/*.qml", 24 | "ressources/*.js", 25 | ] 26 | fileTags: ["ressources"] 27 | } 28 | 29 | Group { 30 | fileTagsFilter: ["dynamiclibrary", "ressources"] 31 | qbs.install: true 32 | qbs.installDir: "QmlPlotting" 33 | qbs.installPrefix: "" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/plugin/plugin.cpp: -------------------------------------------------------------------------------- 1 | #include "../qmlplotting/colormappedimage.h" 2 | #include "../qmlplotting/datasource.h" 3 | #include "../qmlplotting/sliceplot.h" 4 | #include "../qmlplotting/xyplot.h" 5 | #include "../qmlplotting/plotgroup.h" 6 | 7 | #include 8 | #include 9 | 10 | 11 | class QmlPlottingPlugin : public QQmlExtensionPlugin 12 | { 13 | Q_OBJECT 14 | Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) 15 | 16 | public: 17 | QmlPlottingPlugin(QObject* parent = nullptr) : QQmlExtensionPlugin(parent) {} 18 | 19 | void registerTypes(const char *uri) Q_DECL_OVERRIDE 20 | { 21 | Q_ASSERT(QLatin1String(uri) == QLatin1String("QmlPlotting")); 22 | Q_UNUSED(uri); 23 | 24 | qmlRegisterType(uri, 2, 0, "ColormappedImage"); 25 | qmlRegisterType(uri, 2, 0, "DataSource"); 26 | qmlRegisterType(uri, 2, 0, "SlicePlot"); 27 | qmlRegisterType(uri, 2, 0, "XYPlot"); 28 | qmlRegisterType(uri, 2, 0, "PlotGroup"); 29 | } 30 | }; 31 | 32 | #include "plugin.moc" 33 | -------------------------------------------------------------------------------- /src/qmlplotting/colormappedimage.cpp: -------------------------------------------------------------------------------- 1 | #include "colormappedimage.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "qsgdatatexture.h" 11 | #include "colormaps.h" 12 | 13 | #define GLSL(ver, src) "#version " #ver "\n" #src 14 | 15 | // ---------------------------------------------------------------------------- 16 | 17 | class QSQColormapMaterial : public QSGMaterial 18 | { 19 | public: 20 | QSGMaterialType *type() const override { static QSGMaterialType type; return &type; } 21 | QSGMaterialShader *createShader() const override; 22 | QSGTexture* m_texture_image; 23 | QSGDataTexture m_texture_cmap; 24 | double m_amplitude; 25 | double m_offset; 26 | QSGTexture::Filtering m_filter; 27 | }; 28 | 29 | class QSQColormapShader : public QSGMaterialShader 30 | { 31 | public: 32 | const char *vertexShader() const override { 33 | return GLSL(100, 34 | attribute highp vec4 vertex; 35 | attribute highp vec2 texcoord; 36 | uniform highp mat4 matrix; 37 | varying highp vec2 coord; 38 | 39 | void main() { 40 | coord = texcoord; 41 | gl_Position = matrix * vertex; 42 | } 43 | ); 44 | } 45 | 46 | const char *fragmentShader() const override { 47 | return R"( 48 | uniform sampler2D cmap; 49 | uniform sampler2D image; 50 | uniform highp float amplitude; 51 | uniform highp float offset; 52 | uniform lowp float opacity; 53 | varying highp vec2 coord; 54 | 55 | void main() { 56 | bool inside = coord.s > 0. && coord.s < 1. && coord.t > 0. && coord.t < 1.; 57 | highp float val = texture2D(image, coord.st).r; 58 | val = amplitude * (val + offset); 59 | lowp vec4 color = texture2D(cmap, vec2(val, 0.)); 60 | lowp float o = opacity * color.a * float(inside); 61 | gl_FragColor.rgb = color.rgb * o; 62 | gl_FragColor.a = o; 63 | } 64 | )"; 65 | } 66 | 67 | char const *const *attributeNames() const override 68 | { 69 | static char const *const names[] = { "vertex", "texcoord", nullptr }; 70 | return names; 71 | } 72 | 73 | void initialize() override 74 | { 75 | QSGMaterialShader::initialize(); 76 | m_id_matrix = program()->uniformLocation("matrix"); 77 | m_id_opacity = program()->uniformLocation("opacity"); 78 | m_id_image = program()->uniformLocation("image"); 79 | m_id_cmap = program()->uniformLocation("cmap"); 80 | m_id_amplitude = program()->uniformLocation("amplitude"); 81 | m_id_offset = program()->uniformLocation("offset"); 82 | } 83 | 84 | void activate() override { 85 | } 86 | 87 | void updateState(const RenderState& state, QSGMaterial* newMaterial, QSGMaterial*) override 88 | { 89 | Q_ASSERT(program()->isLinked()); 90 | auto* material = static_cast(newMaterial); 91 | QOpenGLFunctions* functions = state.context()->functions(); 92 | 93 | if (state.isMatrixDirty()) { 94 | program()->setUniformValue(m_id_matrix, state.combinedMatrix()); 95 | } 96 | if (state.isOpacityDirty()) { 97 | program()->setUniformValue(m_id_opacity, state.opacity()); 98 | } 99 | 100 | // Bind material parameters 101 | program()->setUniformValue(m_id_amplitude, float(material->m_amplitude)); 102 | program()->setUniformValue(m_id_offset, float(material->m_offset)); 103 | 104 | // Bind the material textures (image and colormap) 105 | functions->glActiveTexture(GL_TEXTURE1); 106 | program()->setUniformValue(m_id_cmap, 1); 107 | material->m_texture_cmap.bind(); 108 | functions->glActiveTexture(GL_TEXTURE0); 109 | program()->setUniformValue(m_id_image, 0); 110 | material->m_texture_image->setFiltering(material->m_filter); 111 | material->m_texture_image->bind(); 112 | } 113 | 114 | void deactivate() override { 115 | // unbind all textures 116 | QOpenGLFunctions* functions = QOpenGLContext::currentContext()->functions(); 117 | functions->glActiveTexture(GL_TEXTURE1); 118 | functions->glBindTexture(GL_TEXTURE_2D, 0); 119 | functions->glActiveTexture(GL_TEXTURE0); 120 | functions->glBindTexture(GL_TEXTURE_2D, 0); 121 | } 122 | 123 | private: 124 | int m_id_matrix; 125 | int m_id_opacity; 126 | int m_id_image; 127 | int m_id_cmap; 128 | int m_id_amplitude; 129 | int m_id_offset; 130 | }; 131 | 132 | 133 | inline QSGMaterialShader* QSQColormapMaterial::createShader() const { return new QSQColormapShader; } 134 | 135 | // ---------------------------------------------------------------------------- 136 | 137 | 138 | ColormappedImage::ColormappedImage(QQuickItem *parent) : DataClient(parent) 139 | { 140 | setFlag(QQuickItem::ItemHasContents); 141 | } 142 | 143 | ColormappedImage::~ColormappedImage() 144 | { 145 | if (m_texture_cmap != nullptr) { 146 | m_texture_cmap->deleteLater(); 147 | } 148 | } 149 | 150 | void ColormappedImage::setMinimumValue(double value) 151 | { 152 | if (value != m_min_value) { 153 | m_min_value = value; 154 | emit minimumValueChanged(value); 155 | update(); 156 | } 157 | } 158 | 159 | void ColormappedImage::setMaximumValue(double value) 160 | { 161 | if (value != m_max_value) { 162 | m_max_value = value; 163 | emit maximumValueChanged(value); 164 | update(); 165 | } 166 | } 167 | 168 | void ColormappedImage::setViewRect(const QRectF& viewrect) 169 | { 170 | if (viewrect != m_view_rect) { 171 | m_view_rect = viewrect; 172 | m_new_geometry = true; 173 | emit viewRectChanged(m_view_rect); 174 | update(); 175 | } 176 | } 177 | 178 | void ColormappedImage::setExtent(const QVector4D& extent) 179 | { 180 | if (extent != m_extent) { 181 | m_extent = extent; 182 | m_new_geometry = true; 183 | emit extentChanged(extent); 184 | update(); 185 | } 186 | } 187 | 188 | void ColormappedImage::setColormap(const QString &colormap) 189 | { 190 | if (colormap != m_colormap) { 191 | m_colormap = colormap; 192 | m_new_colormap = true; 193 | emit colormapChanged(m_colormap); 194 | update(); 195 | } 196 | } 197 | 198 | void ColormappedImage::setFilter(const QString &filter) 199 | { 200 | QSGTexture::Filtering new_filter = (filter == QStringLiteral("linear")) ? QSGTexture::Linear : QSGTexture::Nearest; 201 | if (new_filter != m_filter) { 202 | m_filter = new_filter; 203 | emit filterChanged(getFilter()); 204 | update(); 205 | } 206 | } 207 | 208 | QString ColormappedImage::getFilter() const 209 | { 210 | return (m_filter == QSGTexture::Linear) ? QStringLiteral("linear") : QStringLiteral("nearest"); 211 | } 212 | 213 | static void updateColormapTexture(QSGDataTexture& texture, const QString& colormap) { 214 | texture.setFiltering(QSGTexture::Linear); 215 | 216 | // Get colormap data pointer and number of samples by name 217 | double* data; 218 | int numpoints; 219 | if (colormap == QStringLiteral("wjet")) { 220 | data = const_cast(cmap_wjet); 221 | numpoints = sizeof(cmap_wjet) / (3*sizeof(double)); 222 | } else if (colormap == QStringLiteral("jet")) { 223 | data = const_cast(cmap_jet); 224 | numpoints = sizeof(cmap_jet) / (3*sizeof(double)); 225 | } else if (colormap == QStringLiteral("turbo")) { 226 | data = const_cast(cmap_turbo); 227 | numpoints = sizeof(cmap_turbo) / (3*sizeof(double)); 228 | } else if (colormap == QStringLiteral("hot")) { 229 | data = const_cast(cmap_hot); 230 | numpoints = sizeof(cmap_hot) / (3*sizeof(double)); 231 | } else if (colormap == QStringLiteral("bwr")) { 232 | data = const_cast(cmap_bwr); 233 | numpoints = sizeof(cmap_bwr) / (3*sizeof(double)); 234 | } else if (colormap == QStringLiteral("viridis")) { 235 | data = const_cast(cmap_viridis); 236 | numpoints = sizeof(cmap_viridis) / (3*sizeof(double)); 237 | } else if (colormap == QStringLiteral("ferrugineus")) { 238 | data = const_cast(cmap_ferrugineus); 239 | numpoints = sizeof(cmap_ferrugineus) / (3*sizeof(double)); 240 | } else { 241 | data = const_cast(cmap_gray); 242 | numpoints = sizeof(cmap_gray) / (3*sizeof(double)); 243 | } 244 | 245 | // Copy colormap to data texture 246 | float* textureBuffer = texture.allocateData2D(numpoints, 1, 3); 247 | for (int i = 0; i < numpoints*3; ++i) { 248 | textureBuffer[i] = static_cast(data[i]); 249 | } 250 | texture.commitData(); 251 | } 252 | 253 | QSGNode* ColormappedImage::updatePaintNode(QSGNode* n, QQuickItem::UpdatePaintNodeData*) 254 | { 255 | QSGGeometryNode* n_geom; 256 | QSGGeometry* geometry; 257 | QSQColormapMaterial* material; 258 | 259 | if (n == nullptr) { 260 | n = new QSGNode; 261 | } 262 | 263 | if (m_source == nullptr) { 264 | // Remove child node if there is no data source 265 | if (n->firstChild() != nullptr) { 266 | n_geom = static_cast(n->firstChild()); 267 | n->removeAllChildNodes(); 268 | delete n_geom; 269 | } 270 | // Return empty node 271 | return n; 272 | } 273 | 274 | if (n->firstChild() == nullptr) { 275 | // Create child node if there is a data source 276 | n_geom = new QSGGeometryNode(); 277 | n_geom->setFlag(QSGNode::OwnedByParent); 278 | // Inintialize geometry 279 | geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); 280 | geometry->setDrawingMode(GL_TRIANGLE_STRIP); 281 | n_geom->setGeometry(geometry); 282 | n_geom->setFlag(QSGNode::OwnsGeometry); 283 | m_new_geometry = true; 284 | // Initialize material 285 | material = new QSQColormapMaterial; 286 | material->m_texture_image = m_source->textureProvider()->texture(); 287 | n_geom->setMaterial(material); 288 | n_geom->setFlag(QSGNode::OwnsMaterial); 289 | // Force colormap initialization 290 | m_new_colormap = true; 291 | // 292 | n->appendChildNode(n_geom); 293 | } 294 | // Graph node and data source can be considered valid from here on 295 | 296 | n_geom = static_cast(n->firstChild()); 297 | geometry = n_geom->geometry(); 298 | material = static_cast(n_geom->material()); 299 | QSGNode::DirtyState dirty_state = QSGNode::DirtyMaterial; 300 | 301 | // Check for geometry changes 302 | if (m_new_geometry) { 303 | // Map function for view/extent to texture coordinates (single dimension) 304 | const auto toTexCoords = [](const float v1, const float v_delta, const float e1, const float e2) -> std::tuple { 305 | const float v_center = v1 + .5f * v_delta; 306 | const float e_center = .5f * (e1 + e2); 307 | const float e_delta = e2 - e1; 308 | const float x_delta = v_delta / e_delta; 309 | const float tex_center = (v_center - e_center) / e_delta + .5f; 310 | return {tex_center - .5f * x_delta, tex_center + .5f * x_delta}; 311 | }; 312 | const auto xCoords = toTexCoords(m_view_rect.x(), m_view_rect.width(), m_extent[0], m_extent[1]); 313 | const auto yCoords = toTexCoords(m_view_rect.y(), m_view_rect.height(), m_extent[2], m_extent[3]); 314 | // Enable blending if any texture coordinate is outside [0, 1] 315 | const bool blend = (std::get<0>(xCoords) < 0) || (std::get<0>(xCoords) > 1) || (std::get<1>(xCoords) < 0) || (std::get<1>(xCoords) > 1) 316 | || (std::get<0>(yCoords) < 0) || (std::get<0>(yCoords) > 1) || (std::get<1>(yCoords) < 0) || (std::get<1>(yCoords) > 1); 317 | material->setFlag(QSGMaterial::Blending, blend); 318 | // Update geometry and texture coordinates 319 | const float w = static_cast(width()); 320 | const float h = static_cast(height()); 321 | geometry->vertexDataAsTexturedPoint2D()[0] = {0, 0, std::get<0>(xCoords), std::get<1>(yCoords)}; 322 | geometry->vertexDataAsTexturedPoint2D()[1] = {0, h, std::get<0>(xCoords), std::get<0>(yCoords)}; 323 | geometry->vertexDataAsTexturedPoint2D()[2] = {w, 0, std::get<1>(xCoords), std::get<1>(yCoords)}; 324 | geometry->vertexDataAsTexturedPoint2D()[3] = {w, h, std::get<1>(xCoords), std::get<0>(yCoords)}; 325 | dirty_state |= QSGNode::DirtyGeometry; 326 | m_new_geometry = false; 327 | } 328 | 329 | // Check for data source change 330 | if (m_new_source) { 331 | material->m_texture_image = m_source->textureProvider()->texture(); 332 | m_new_source = false; 333 | } 334 | 335 | // Check if the image texture should be updated 336 | if (m_new_data) { 337 | static_cast(material->m_texture_image)->updateTexture(); 338 | m_new_data = false; 339 | } 340 | 341 | // Check if the colormap should be updated 342 | if (m_new_colormap) { 343 | updateColormapTexture(material->m_texture_cmap, m_colormap); 344 | m_new_colormap = false; 345 | } 346 | 347 | // Update material parameters 348 | double cmap_margin = .5 / material->m_texture_cmap.getDim(0); 349 | material->m_amplitude = (1. - 2.*cmap_margin) / (m_max_value - m_min_value); 350 | material->m_offset = (cmap_margin / material->m_amplitude) - m_min_value; 351 | material->m_filter = m_filter; 352 | 353 | n->markDirty(dirty_state); 354 | n_geom->markDirty(dirty_state); 355 | return n; 356 | } 357 | -------------------------------------------------------------------------------- /src/qmlplotting/colormappedimage.h: -------------------------------------------------------------------------------- 1 | #ifndef COLORMAPPEDIMAGE_H 2 | #define COLORMAPPEDIMAGE_H 3 | 4 | #include "dataclient.h" 5 | #include 6 | 7 | class ColormappedImage : public DataClient 8 | { 9 | Q_OBJECT 10 | Q_PROPERTY(double minimumValue MEMBER m_min_value WRITE setMinimumValue NOTIFY minimumValueChanged) 11 | Q_PROPERTY(double maximumValue MEMBER m_max_value WRITE setMaximumValue NOTIFY maximumValueChanged) 12 | Q_PROPERTY(QRectF viewRect MEMBER m_view_rect WRITE setViewRect NOTIFY viewRectChanged) 13 | Q_PROPERTY(QVector4D extent MEMBER m_extent WRITE setExtent NOTIFY extentChanged) 14 | Q_PROPERTY(QString colormap MEMBER m_colormap WRITE setColormap NOTIFY colormapChanged) 15 | Q_PROPERTY(QString filter READ getFilter WRITE setFilter NOTIFY filterChanged) 16 | 17 | public: 18 | explicit ColormappedImage(QQuickItem *parent = nullptr); 19 | ~ColormappedImage() override; 20 | 21 | void setMinimumValue(double value); 22 | void setMaximumValue(double value); 23 | void setViewRect(const QRectF& viewrect); 24 | void setExtent(const QVector4D& extent); 25 | void setColormap(const QString& colormap); 26 | void setFilter(const QString& filter); 27 | QString getFilter() const; 28 | 29 | signals: 30 | void minimumValueChanged(double value); 31 | void maximumValueChanged(double value); 32 | void viewRectChanged(const QRectF& viewrect); 33 | void extentChanged(const QVector4D& extent); 34 | void colormapChanged(const QString& colormap); 35 | void filterChanged(const QString& filter); 36 | 37 | protected: 38 | QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* updatePaintNodeData) override; 39 | 40 | private: 41 | double m_min_value = 0.; 42 | double m_max_value = 1.; 43 | QRectF m_view_rect = {0., 0., 1., 1.}; 44 | QVector4D m_extent = {0., 1., 0., 1.}; 45 | QString m_colormap; 46 | bool m_new_colormap = false; 47 | QSGTexture* m_texture_cmap = nullptr; 48 | QSGTexture::Filtering m_filter = QSGTexture::Linear; 49 | }; 50 | 51 | 52 | #endif // COLORMAPPEDIMAGE_H 53 | 54 | -------------------------------------------------------------------------------- /src/qmlplotting/colormaps.h: -------------------------------------------------------------------------------- 1 | #ifndef COLORMAPS_H 2 | #define COLORMAPS_H 3 | 4 | constexpr double cmap_wjet[] = { 5 | 1.000000, 1.000000, 1.000000, 6 | 0.200000, 0.300000, 1.000000, 7 | 0.000000, 0.500000, 1.000000, 8 | 0.000000, 1.000000, 1.000000, 9 | 0.500000, 1.000000, 0.000000, 10 | 1.000000, 1.000000, 0.000000, 11 | 1.000000, 0.300000, 0.000000, 12 | 1.000000, 0.000000, 0.000000 13 | }; 14 | 15 | constexpr double cmap_jet[] = { 16 | 0.000000, 0.000000, 0.500000, 17 | 0.000000, 0.000000, 1.000000, 18 | 0.000000, 1.000000, 1.000000, 19 | 0.500000, 1.000000, 0.300000, 20 | 1.000000, 1.000000, 0.000000, 21 | 1.000000, 0.000000, 0.000000 22 | }; 23 | 24 | // https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html 25 | constexpr double cmap_turbo[] = { 26 | 0.18995, 0.07176, 0.23217, 27 | 0.19483, 0.08339, 0.26149, 28 | 0.19956, 0.09498, 0.29024, 29 | 0.20415, 0.10652, 0.31844, 30 | 0.20860, 0.11802, 0.34607, 31 | 0.21291, 0.12947, 0.37314, 32 | 0.21708, 0.14087, 0.39964, 33 | 0.22111, 0.15223, 0.42558, 34 | 0.22500, 0.16354, 0.45096, 35 | 0.22875, 0.17481, 0.47578, 36 | 0.23236, 0.18603, 0.50004, 37 | 0.23582, 0.19720, 0.52373, 38 | 0.23915, 0.20833, 0.54686, 39 | 0.24234, 0.21941, 0.56942, 40 | 0.24539, 0.23044, 0.59142, 41 | 0.24830, 0.24143, 0.61286, 42 | 0.25107, 0.25237, 0.63374, 43 | 0.25369, 0.26327, 0.65406, 44 | 0.25618, 0.27412, 0.67381, 45 | 0.25853, 0.28492, 0.69300, 46 | 0.26074, 0.29568, 0.71162, 47 | 0.26280, 0.30639, 0.72968, 48 | 0.26473, 0.31706, 0.74718, 49 | 0.26652, 0.32768, 0.76412, 50 | 0.26816, 0.33825, 0.78050, 51 | 0.26967, 0.34878, 0.79631, 52 | 0.27103, 0.35926, 0.81156, 53 | 0.27226, 0.36970, 0.82624, 54 | 0.27334, 0.38008, 0.84037, 55 | 0.27429, 0.39043, 0.85393, 56 | 0.27509, 0.40072, 0.86692, 57 | 0.27576, 0.41097, 0.87936, 58 | 0.27628, 0.42118, 0.89123, 59 | 0.27667, 0.43134, 0.90254, 60 | 0.27691, 0.44145, 0.91328, 61 | 0.27701, 0.45152, 0.92347, 62 | 0.27698, 0.46153, 0.93309, 63 | 0.27680, 0.47151, 0.94214, 64 | 0.27648, 0.48144, 0.95064, 65 | 0.27603, 0.49132, 0.95857, 66 | 0.27543, 0.50115, 0.96594, 67 | 0.27469, 0.51094, 0.97275, 68 | 0.27381, 0.52069, 0.97899, 69 | 0.27273, 0.53040, 0.98461, 70 | 0.27106, 0.54015, 0.98930, 71 | 0.26878, 0.54995, 0.99303, 72 | 0.26592, 0.55979, 0.99583, 73 | 0.26252, 0.56967, 0.99773, 74 | 0.25862, 0.57958, 0.99876, 75 | 0.25425, 0.58950, 0.99896, 76 | 0.24946, 0.59943, 0.99835, 77 | 0.24427, 0.60937, 0.99697, 78 | 0.23874, 0.61931, 0.99485, 79 | 0.23288, 0.62923, 0.99202, 80 | 0.22676, 0.63913, 0.98851, 81 | 0.22039, 0.64901, 0.98436, 82 | 0.21382, 0.65886, 0.97959, 83 | 0.20708, 0.66866, 0.97423, 84 | 0.20021, 0.67842, 0.96833, 85 | 0.19326, 0.68812, 0.96190, 86 | 0.18625, 0.69775, 0.95498, 87 | 0.17923, 0.70732, 0.94761, 88 | 0.17223, 0.71680, 0.93981, 89 | 0.16529, 0.72620, 0.93161, 90 | 0.15844, 0.73551, 0.92305, 91 | 0.15173, 0.74472, 0.91416, 92 | 0.14519, 0.75381, 0.90496, 93 | 0.13886, 0.76279, 0.89550, 94 | 0.13278, 0.77165, 0.88580, 95 | 0.12698, 0.78037, 0.87590, 96 | 0.12151, 0.78896, 0.86581, 97 | 0.11639, 0.79740, 0.85559, 98 | 0.11167, 0.80569, 0.84525, 99 | 0.10738, 0.81381, 0.83484, 100 | 0.10357, 0.82177, 0.82437, 101 | 0.10026, 0.82955, 0.81389, 102 | 0.09750, 0.83714, 0.80342, 103 | 0.09532, 0.84455, 0.79299, 104 | 0.09377, 0.85175, 0.78264, 105 | 0.09287, 0.85875, 0.77240, 106 | 0.09267, 0.86554, 0.76230, 107 | 0.09320, 0.87211, 0.75237, 108 | 0.09451, 0.87844, 0.74265, 109 | 0.09662, 0.88454, 0.73316, 110 | 0.09958, 0.89040, 0.72393, 111 | 0.10342, 0.89600, 0.71500, 112 | 0.10815, 0.90142, 0.70599, 113 | 0.11374, 0.90673, 0.69651, 114 | 0.12014, 0.91193, 0.68660, 115 | 0.12733, 0.91701, 0.67627, 116 | 0.13526, 0.92197, 0.66556, 117 | 0.14391, 0.92680, 0.65448, 118 | 0.15323, 0.93151, 0.64308, 119 | 0.16319, 0.93609, 0.63137, 120 | 0.17377, 0.94053, 0.61938, 121 | 0.18491, 0.94484, 0.60713, 122 | 0.19659, 0.94901, 0.59466, 123 | 0.20877, 0.95304, 0.58199, 124 | 0.22142, 0.95692, 0.56914, 125 | 0.23449, 0.96065, 0.55614, 126 | 0.24797, 0.96423, 0.54303, 127 | 0.26180, 0.96765, 0.52981, 128 | 0.27597, 0.97092, 0.51653, 129 | 0.29042, 0.97403, 0.50321, 130 | 0.30513, 0.97697, 0.48987, 131 | 0.32006, 0.97974, 0.47654, 132 | 0.33517, 0.98234, 0.46325, 133 | 0.35043, 0.98477, 0.45002, 134 | 0.36581, 0.98702, 0.43688, 135 | 0.38127, 0.98909, 0.42386, 136 | 0.39678, 0.99098, 0.41098, 137 | 0.41229, 0.99268, 0.39826, 138 | 0.42778, 0.99419, 0.38575, 139 | 0.44321, 0.99551, 0.37345, 140 | 0.45854, 0.99663, 0.36140, 141 | 0.47375, 0.99755, 0.34963, 142 | 0.48879, 0.99828, 0.33816, 143 | 0.50362, 0.99879, 0.32701, 144 | 0.51822, 0.99910, 0.31622, 145 | 0.53255, 0.99919, 0.30581, 146 | 0.54658, 0.99907, 0.29581, 147 | 0.56026, 0.99873, 0.28623, 148 | 0.57357, 0.99817, 0.27712, 149 | 0.58646, 0.99739, 0.26849, 150 | 0.59891, 0.99638, 0.26038, 151 | 0.61088, 0.99514, 0.25280, 152 | 0.62233, 0.99366, 0.24579, 153 | 0.63323, 0.99195, 0.23937, 154 | 0.64362, 0.98999, 0.23356, 155 | 0.65394, 0.98775, 0.22835, 156 | 0.66428, 0.98524, 0.22370, 157 | 0.67462, 0.98246, 0.21960, 158 | 0.68494, 0.97941, 0.21602, 159 | 0.69525, 0.97610, 0.21294, 160 | 0.70553, 0.97255, 0.21032, 161 | 0.71577, 0.96875, 0.20815, 162 | 0.72596, 0.96470, 0.20640, 163 | 0.73610, 0.96043, 0.20504, 164 | 0.74617, 0.95593, 0.20406, 165 | 0.75617, 0.95121, 0.20343, 166 | 0.76608, 0.94627, 0.20311, 167 | 0.77591, 0.94113, 0.20310, 168 | 0.78563, 0.93579, 0.20336, 169 | 0.79524, 0.93025, 0.20386, 170 | 0.80473, 0.92452, 0.20459, 171 | 0.81410, 0.91861, 0.20552, 172 | 0.82333, 0.91253, 0.20663, 173 | 0.83241, 0.90627, 0.20788, 174 | 0.84133, 0.89986, 0.20926, 175 | 0.85010, 0.89328, 0.21074, 176 | 0.85868, 0.88655, 0.21230, 177 | 0.86709, 0.87968, 0.21391, 178 | 0.87530, 0.87267, 0.21555, 179 | 0.88331, 0.86553, 0.21719, 180 | 0.89112, 0.85826, 0.21880, 181 | 0.89870, 0.85087, 0.22038, 182 | 0.90605, 0.84337, 0.22188, 183 | 0.91317, 0.83576, 0.22328, 184 | 0.92004, 0.82806, 0.22456, 185 | 0.92666, 0.82025, 0.22570, 186 | 0.93301, 0.81236, 0.22667, 187 | 0.93909, 0.80439, 0.22744, 188 | 0.94489, 0.79634, 0.22800, 189 | 0.95039, 0.78823, 0.22831, 190 | 0.95560, 0.78005, 0.22836, 191 | 0.96049, 0.77181, 0.22811, 192 | 0.96507, 0.76352, 0.22754, 193 | 0.96931, 0.75519, 0.22663, 194 | 0.97323, 0.74682, 0.22536, 195 | 0.97679, 0.73842, 0.22369, 196 | 0.98000, 0.73000, 0.22161, 197 | 0.98289, 0.72140, 0.21918, 198 | 0.98549, 0.71250, 0.21650, 199 | 0.98781, 0.70330, 0.21358, 200 | 0.98986, 0.69382, 0.21043, 201 | 0.99163, 0.68408, 0.20706, 202 | 0.99314, 0.67408, 0.20348, 203 | 0.99438, 0.66386, 0.19971, 204 | 0.99535, 0.65341, 0.19577, 205 | 0.99607, 0.64277, 0.19165, 206 | 0.99654, 0.63193, 0.18738, 207 | 0.99675, 0.62093, 0.18297, 208 | 0.99672, 0.60977, 0.17842, 209 | 0.99644, 0.59846, 0.17376, 210 | 0.99593, 0.58703, 0.16899, 211 | 0.99517, 0.57549, 0.16412, 212 | 0.99419, 0.56386, 0.15918, 213 | 0.99297, 0.55214, 0.15417, 214 | 0.99153, 0.54036, 0.14910, 215 | 0.98987, 0.52854, 0.14398, 216 | 0.98799, 0.51667, 0.13883, 217 | 0.98590, 0.50479, 0.13367, 218 | 0.98360, 0.49291, 0.12849, 219 | 0.98108, 0.48104, 0.12332, 220 | 0.97837, 0.46920, 0.11817, 221 | 0.97545, 0.45740, 0.11305, 222 | 0.97234, 0.44565, 0.10797, 223 | 0.96904, 0.43399, 0.10294, 224 | 0.96555, 0.42241, 0.09798, 225 | 0.96187, 0.41093, 0.09310, 226 | 0.95801, 0.39958, 0.08831, 227 | 0.95398, 0.38836, 0.08362, 228 | 0.94977, 0.37729, 0.07905, 229 | 0.94538, 0.36638, 0.07461, 230 | 0.94084, 0.35566, 0.07031, 231 | 0.93612, 0.34513, 0.06616, 232 | 0.93125, 0.33482, 0.06218, 233 | 0.92623, 0.32473, 0.05837, 234 | 0.92105, 0.31489, 0.05475, 235 | 0.91572, 0.30530, 0.05134, 236 | 0.91024, 0.29599, 0.04814, 237 | 0.90463, 0.28696, 0.04516, 238 | 0.89888, 0.27824, 0.04243, 239 | 0.89298, 0.26981, 0.03993, 240 | 0.88691, 0.26152, 0.03753, 241 | 0.88066, 0.25334, 0.03521, 242 | 0.87422, 0.24526, 0.03297, 243 | 0.86760, 0.23730, 0.03082, 244 | 0.86079, 0.22945, 0.02875, 245 | 0.85380, 0.22170, 0.02677, 246 | 0.84662, 0.21407, 0.02487, 247 | 0.83926, 0.20654, 0.02305, 248 | 0.83172, 0.19912, 0.02131, 249 | 0.82399, 0.19182, 0.01966, 250 | 0.81608, 0.18462, 0.01809, 251 | 0.80799, 0.17753, 0.01660, 252 | 0.79971, 0.17055, 0.01520, 253 | 0.79125, 0.16368, 0.01387, 254 | 0.78260, 0.15693, 0.01264, 255 | 0.77377, 0.15028, 0.01148, 256 | 0.76476, 0.14374, 0.01041, 257 | 0.75556, 0.13731, 0.00942, 258 | 0.74617, 0.13098, 0.00851, 259 | 0.73661, 0.12477, 0.00769, 260 | 0.72686, 0.11867, 0.00695, 261 | 0.71692, 0.11268, 0.00629, 262 | 0.70680, 0.10680, 0.00571, 263 | 0.69650, 0.10102, 0.00522, 264 | 0.68602, 0.09536, 0.00481, 265 | 0.67535, 0.08980, 0.00449, 266 | 0.66449, 0.08436, 0.00424, 267 | 0.65345, 0.07902, 0.00408, 268 | 0.64223, 0.07380, 0.00401, 269 | 0.63082, 0.06868, 0.00401, 270 | 0.61923, 0.06367, 0.00410, 271 | 0.60746, 0.05878, 0.00427, 272 | 0.59550, 0.05399, 0.00453, 273 | 0.58336, 0.04931, 0.00486, 274 | 0.57103, 0.04474, 0.00529, 275 | 0.55852, 0.04028, 0.00579, 276 | 0.54583, 0.03593, 0.00638, 277 | 0.53295, 0.03169, 0.00705, 278 | 0.51989, 0.02756, 0.00780, 279 | 0.50664, 0.02354, 0.00863, 280 | 0.49321, 0.01963, 0.00955, 281 | 0.47960, 0.01583, 0.01055, 282 | }; 283 | 284 | constexpr double cmap_hot[] = { 285 | 0.000000, 0.000000, 0.000000, 286 | 0.800000, 0.000000, 0.000000, 287 | 1.000000, 0.900000, 0.000000, 288 | 1.000000, 1.000000, 1.000000 289 | }; 290 | 291 | constexpr double cmap_bwr[] = { 292 | 0.229806, 0.298718, 0.753683, 293 | 0.266234, 0.353095, 0.801467, 294 | 0.303869, 0.406535, 0.844959, 295 | 0.342804, 0.458758, 0.883726, 296 | 0.383013, 0.509419, 0.917388, 297 | 0.424370, 0.558148, 0.945620, 298 | 0.466667, 0.604563, 0.968155, 299 | 0.509635, 0.648281, 0.984788, 300 | 0.552953, 0.688929, 0.995376, 301 | 0.596262, 0.726149, 0.999836, 302 | 0.639176, 0.759600, 0.998151, 303 | 0.681291, 0.788965, 0.990363, 304 | 0.722193, 0.813953, 0.976575, 305 | 0.761465, 0.834303, 0.956945, 306 | 0.798692, 0.849786, 0.931689, 307 | 0.833467, 0.860208, 0.901069, 308 | 0.865395, 0.865410, 0.865396, 309 | 0.897787, 0.848937, 0.820881, 310 | 0.924128, 0.827385, 0.774508, 311 | 0.944469, 0.800927, 0.726736, 312 | 0.958853, 0.769768, 0.678008, 313 | 0.967328, 0.734133, 0.628752, 314 | 0.969954, 0.694267, 0.579375, 315 | 0.966811, 0.650421, 0.530264, 316 | 0.958003, 0.602842, 0.481776, 317 | 0.943661, 0.551751, 0.434244, 318 | 0.923945, 0.497309, 0.387970, 319 | 0.899046, 0.439559, 0.343230, 320 | 0.869187, 0.378313, 0.300267, 321 | 0.834621, 0.312874, 0.259301, 322 | 0.795632, 0.241284, 0.220526, 323 | 0.752535, 0.157246, 0.184115, 324 | 0.705673, 0.015556, 0.150233 325 | }; 326 | 327 | constexpr double cmap_viridis[] = { 328 | 0.267004, 0.004874, 0.329415, 329 | 0.268510, 0.009605, 0.335427, 330 | 0.269944, 0.014625, 0.341379, 331 | 0.271305, 0.019942, 0.347269, 332 | 0.272594, 0.025563, 0.353093, 333 | 0.273809, 0.031497, 0.358853, 334 | 0.274952, 0.037752, 0.364543, 335 | 0.276022, 0.044167, 0.370164, 336 | 0.277018, 0.050344, 0.375715, 337 | 0.277941, 0.056324, 0.381191, 338 | 0.278791, 0.062145, 0.386592, 339 | 0.279566, 0.067836, 0.391917, 340 | 0.280267, 0.073417, 0.397163, 341 | 0.280894, 0.078907, 0.402329, 342 | 0.281446, 0.084320, 0.407414, 343 | 0.281924, 0.089666, 0.412415, 344 | 0.282327, 0.094955, 0.417331, 345 | 0.282656, 0.100196, 0.422160, 346 | 0.282910, 0.105393, 0.426902, 347 | 0.283091, 0.110553, 0.431554, 348 | 0.283197, 0.115680, 0.436115, 349 | 0.283229, 0.120777, 0.440584, 350 | 0.283187, 0.125848, 0.444960, 351 | 0.283072, 0.130895, 0.449241, 352 | 0.282884, 0.135920, 0.453427, 353 | 0.282623, 0.140926, 0.457517, 354 | 0.282290, 0.145912, 0.461510, 355 | 0.281887, 0.150881, 0.465405, 356 | 0.281412, 0.155834, 0.469201, 357 | 0.280868, 0.160771, 0.472899, 358 | 0.280255, 0.165693, 0.476498, 359 | 0.279574, 0.170599, 0.479997, 360 | 0.278826, 0.175490, 0.483397, 361 | 0.278012, 0.180367, 0.486697, 362 | 0.277134, 0.185228, 0.489898, 363 | 0.276194, 0.190074, 0.493001, 364 | 0.275191, 0.194905, 0.496005, 365 | 0.274128, 0.199721, 0.498911, 366 | 0.273006, 0.204520, 0.501721, 367 | 0.271828, 0.209303, 0.504434, 368 | 0.270595, 0.214069, 0.507052, 369 | 0.269308, 0.218818, 0.509577, 370 | 0.267968, 0.223549, 0.512008, 371 | 0.266580, 0.228262, 0.514349, 372 | 0.265145, 0.232956, 0.516599, 373 | 0.263663, 0.237631, 0.518762, 374 | 0.262138, 0.242286, 0.520837, 375 | 0.260571, 0.246922, 0.522828, 376 | 0.258965, 0.251537, 0.524736, 377 | 0.257322, 0.256130, 0.526563, 378 | 0.255645, 0.260703, 0.528312, 379 | 0.253935, 0.265254, 0.529983, 380 | 0.252194, 0.269783, 0.531579, 381 | 0.250425, 0.274290, 0.533103, 382 | 0.248629, 0.278775, 0.534556, 383 | 0.246811, 0.283237, 0.535941, 384 | 0.244972, 0.287675, 0.537260, 385 | 0.243113, 0.292092, 0.538516, 386 | 0.241237, 0.296485, 0.539709, 387 | 0.239346, 0.300855, 0.540844, 388 | 0.237441, 0.305202, 0.541921, 389 | 0.235526, 0.309527, 0.542944, 390 | 0.233603, 0.313828, 0.543914, 391 | 0.231674, 0.318106, 0.544834, 392 | 0.229739, 0.322361, 0.545706, 393 | 0.227802, 0.326594, 0.546532, 394 | 0.225863, 0.330805, 0.547314, 395 | 0.223925, 0.334994, 0.548053, 396 | 0.221989, 0.339161, 0.548752, 397 | 0.220057, 0.343307, 0.549413, 398 | 0.218130, 0.347432, 0.550038, 399 | 0.216210, 0.351535, 0.550627, 400 | 0.214298, 0.355619, 0.551184, 401 | 0.212395, 0.359683, 0.551710, 402 | 0.210503, 0.363727, 0.552206, 403 | 0.208623, 0.367752, 0.552675, 404 | 0.206756, 0.371758, 0.553117, 405 | 0.204903, 0.375746, 0.553533, 406 | 0.203063, 0.379716, 0.553925, 407 | 0.201239, 0.383670, 0.554294, 408 | 0.199430, 0.387607, 0.554642, 409 | 0.197636, 0.391528, 0.554969, 410 | 0.195860, 0.395433, 0.555276, 411 | 0.194100, 0.399323, 0.555565, 412 | 0.192357, 0.403199, 0.555836, 413 | 0.190631, 0.407061, 0.556089, 414 | 0.188923, 0.410910, 0.556326, 415 | 0.187231, 0.414746, 0.556547, 416 | 0.185556, 0.418570, 0.556753, 417 | 0.183898, 0.422383, 0.556944, 418 | 0.182256, 0.426184, 0.557120, 419 | 0.180629, 0.429975, 0.557282, 420 | 0.179019, 0.433756, 0.557430, 421 | 0.177423, 0.437527, 0.557565, 422 | 0.175841, 0.441290, 0.557685, 423 | 0.174274, 0.445044, 0.557792, 424 | 0.172719, 0.448791, 0.557885, 425 | 0.171176, 0.452530, 0.557965, 426 | 0.169646, 0.456262, 0.558030, 427 | 0.168126, 0.459988, 0.558082, 428 | 0.166617, 0.463708, 0.558119, 429 | 0.165117, 0.467423, 0.558141, 430 | 0.163625, 0.471133, 0.558148, 431 | 0.162142, 0.474838, 0.558140, 432 | 0.160665, 0.478540, 0.558115, 433 | 0.159194, 0.482237, 0.558073, 434 | 0.157729, 0.485932, 0.558013, 435 | 0.156270, 0.489624, 0.557936, 436 | 0.154815, 0.493313, 0.557840, 437 | 0.153364, 0.497000, 0.557724, 438 | 0.151918, 0.500685, 0.557587, 439 | 0.150476, 0.504369, 0.557430, 440 | 0.149039, 0.508051, 0.557250, 441 | 0.147607, 0.511733, 0.557049, 442 | 0.146180, 0.515413, 0.556823, 443 | 0.144759, 0.519093, 0.556572, 444 | 0.143343, 0.522773, 0.556295, 445 | 0.141935, 0.526453, 0.555991, 446 | 0.140536, 0.530132, 0.555659, 447 | 0.139147, 0.533812, 0.555298, 448 | 0.137770, 0.537492, 0.554906, 449 | 0.136408, 0.541173, 0.554483, 450 | 0.135066, 0.544853, 0.554029, 451 | 0.133743, 0.548535, 0.553541, 452 | 0.132444, 0.552216, 0.553018, 453 | 0.131172, 0.555899, 0.552459, 454 | 0.129933, 0.559582, 0.551864, 455 | 0.128729, 0.563265, 0.551229, 456 | 0.127568, 0.566949, 0.550556, 457 | 0.126453, 0.570633, 0.549841, 458 | 0.125394, 0.574318, 0.549086, 459 | 0.124395, 0.578002, 0.548287, 460 | 0.123463, 0.581687, 0.547445, 461 | 0.122606, 0.585371, 0.546557, 462 | 0.121831, 0.589055, 0.545623, 463 | 0.121148, 0.592739, 0.544641, 464 | 0.120565, 0.596422, 0.543611, 465 | 0.120092, 0.600104, 0.542530, 466 | 0.119738, 0.603785, 0.541400, 467 | 0.119512, 0.607464, 0.540218, 468 | 0.119423, 0.611141, 0.538982, 469 | 0.119483, 0.614817, 0.537692, 470 | 0.119699, 0.618490, 0.536347, 471 | 0.120081, 0.622161, 0.534946, 472 | 0.120638, 0.625828, 0.533488, 473 | 0.121380, 0.629492, 0.531973, 474 | 0.122312, 0.633153, 0.530398, 475 | 0.123444, 0.636809, 0.528763, 476 | 0.124780, 0.640461, 0.527068, 477 | 0.126326, 0.644107, 0.525311, 478 | 0.128087, 0.647749, 0.523491, 479 | 0.130067, 0.651384, 0.521608, 480 | 0.132268, 0.655014, 0.519661, 481 | 0.134692, 0.658636, 0.517649, 482 | 0.137339, 0.662252, 0.515571, 483 | 0.140210, 0.665859, 0.513427, 484 | 0.143303, 0.669459, 0.511215, 485 | 0.146616, 0.673050, 0.508936, 486 | 0.150148, 0.676631, 0.506589, 487 | 0.153894, 0.680203, 0.504172, 488 | 0.157851, 0.683765, 0.501686, 489 | 0.162016, 0.687316, 0.499129, 490 | 0.166383, 0.690856, 0.496502, 491 | 0.170948, 0.694384, 0.493803, 492 | 0.175707, 0.697900, 0.491033, 493 | 0.180653, 0.701402, 0.488189, 494 | 0.185783, 0.704891, 0.485273, 495 | 0.191090, 0.708366, 0.482284, 496 | 0.196571, 0.711827, 0.479221, 497 | 0.202219, 0.715272, 0.476084, 498 | 0.208030, 0.718701, 0.472873, 499 | 0.214000, 0.722114, 0.469588, 500 | 0.220124, 0.725509, 0.466226, 501 | 0.226397, 0.728888, 0.462789, 502 | 0.232815, 0.732247, 0.459277, 503 | 0.239374, 0.735588, 0.455688, 504 | 0.246070, 0.738910, 0.452024, 505 | 0.252899, 0.742211, 0.448284, 506 | 0.259857, 0.745492, 0.444467, 507 | 0.266941, 0.748751, 0.440573, 508 | 0.274149, 0.751988, 0.436601, 509 | 0.281477, 0.755203, 0.432552, 510 | 0.288921, 0.758394, 0.428426, 511 | 0.296479, 0.761561, 0.424223, 512 | 0.304148, 0.764704, 0.419943, 513 | 0.311925, 0.767822, 0.415586, 514 | 0.319809, 0.770914, 0.411152, 515 | 0.327796, 0.773980, 0.406640, 516 | 0.335885, 0.777018, 0.402049, 517 | 0.344074, 0.780029, 0.397381, 518 | 0.352360, 0.783011, 0.392636, 519 | 0.360741, 0.785964, 0.387814, 520 | 0.369214, 0.788888, 0.382914, 521 | 0.377779, 0.791781, 0.377939, 522 | 0.386433, 0.794644, 0.372886, 523 | 0.395174, 0.797475, 0.367757, 524 | 0.404001, 0.800275, 0.362552, 525 | 0.412913, 0.803041, 0.357269, 526 | 0.421908, 0.805774, 0.351910, 527 | 0.430983, 0.808473, 0.346476, 528 | 0.440137, 0.811138, 0.340967, 529 | 0.449368, 0.813768, 0.335384, 530 | 0.458674, 0.816363, 0.329727, 531 | 0.468053, 0.818921, 0.323998, 532 | 0.477504, 0.821444, 0.318195, 533 | 0.487026, 0.823929, 0.312321, 534 | 0.496615, 0.826376, 0.306377, 535 | 0.506271, 0.828786, 0.300362, 536 | 0.515992, 0.831158, 0.294279, 537 | 0.525776, 0.833491, 0.288127, 538 | 0.535621, 0.835785, 0.281908, 539 | 0.545524, 0.838039, 0.275626, 540 | 0.555484, 0.840254, 0.269281, 541 | 0.565498, 0.842430, 0.262877, 542 | 0.575563, 0.844566, 0.256415, 543 | 0.585678, 0.846661, 0.249897, 544 | 0.595839, 0.848717, 0.243329, 545 | 0.606045, 0.850733, 0.236712, 546 | 0.616293, 0.852709, 0.230052, 547 | 0.626579, 0.854645, 0.223353, 548 | 0.636902, 0.856542, 0.216620, 549 | 0.647257, 0.858400, 0.209861, 550 | 0.657642, 0.860219, 0.203082, 551 | 0.668054, 0.861999, 0.196293, 552 | 0.678489, 0.863742, 0.189503, 553 | 0.688944, 0.865448, 0.182725, 554 | 0.699415, 0.867117, 0.175971, 555 | 0.709898, 0.868751, 0.169257, 556 | 0.720391, 0.870350, 0.162603, 557 | 0.730889, 0.871916, 0.156029, 558 | 0.741388, 0.873449, 0.149561, 559 | 0.751884, 0.874951, 0.143228, 560 | 0.762373, 0.876424, 0.137064, 561 | 0.772852, 0.877868, 0.131109, 562 | 0.783315, 0.879285, 0.125405, 563 | 0.793760, 0.880678, 0.120005, 564 | 0.804182, 0.882046, 0.114965, 565 | 0.814576, 0.883393, 0.110347, 566 | 0.824940, 0.884720, 0.106217, 567 | 0.835270, 0.886029, 0.102646, 568 | 0.845561, 0.887322, 0.099702, 569 | 0.855810, 0.888601, 0.097452, 570 | 0.866013, 0.889868, 0.095953, 571 | 0.876168, 0.891125, 0.095250, 572 | 0.886271, 0.892374, 0.095374, 573 | 0.896320, 0.893616, 0.096335, 574 | 0.906311, 0.894855, 0.098125, 575 | 0.916242, 0.896091, 0.100717, 576 | 0.926106, 0.897330, 0.104071, 577 | 0.935904, 0.898570, 0.108131, 578 | 0.945636, 0.899815, 0.112838, 579 | 0.955300, 0.901065, 0.118128, 580 | 0.964894, 0.902323, 0.123941, 581 | 0.974417, 0.903590, 0.130215, 582 | 0.983868, 0.904867, 0.136897, 583 | 0.993248, 0.906157, 0.143936, 584 | }; 585 | 586 | constexpr double cmap_ferrugineus[] = { 587 | 0.001462, 0.000466, 0.013866, 588 | 0.002267, 0.001270, 0.018570, 589 | 0.003299, 0.002249, 0.024239, 590 | 0.004547, 0.003392, 0.030909, 591 | 0.006006, 0.004692, 0.038558, 592 | 0.007676, 0.006136, 0.046836, 593 | 0.009561, 0.007713, 0.055143, 594 | 0.011663, 0.009417, 0.063460, 595 | 0.013995, 0.011225, 0.071862, 596 | 0.016561, 0.013136, 0.080282, 597 | 0.019373, 0.015133, 0.088767, 598 | 0.022447, 0.017199, 0.097327, 599 | 0.025793, 0.019331, 0.105930, 600 | 0.029432, 0.021503, 0.114621, 601 | 0.033385, 0.023702, 0.123397, 602 | 0.037668, 0.025921, 0.132232, 603 | 0.042253, 0.028139, 0.141141, 604 | 0.046915, 0.030324, 0.150164, 605 | 0.051644, 0.032474, 0.159254, 606 | 0.056449, 0.034569, 0.168414, 607 | 0.061340, 0.036590, 0.177642, 608 | 0.066331, 0.038504, 0.186962, 609 | 0.071429, 0.040294, 0.196354, 610 | 0.076637, 0.041905, 0.205799, 611 | 0.081962, 0.043328, 0.215289, 612 | 0.087411, 0.044556, 0.224813, 613 | 0.092990, 0.045583, 0.234358, 614 | 0.098702, 0.046402, 0.243904, 615 | 0.104551, 0.047008, 0.253430, 616 | 0.110536, 0.047399, 0.262912, 617 | 0.116656, 0.047574, 0.272321, 618 | 0.122908, 0.047536, 0.281624, 619 | 0.129285, 0.047293, 0.290788, 620 | 0.135778, 0.046856, 0.299776, 621 | 0.142378, 0.046242, 0.308553, 622 | 0.149073, 0.045468, 0.317085, 623 | 0.155850, 0.044559, 0.325338, 624 | 0.162689, 0.043554, 0.333277, 625 | 0.169575, 0.042489, 0.340874, 626 | 0.176493, 0.041402, 0.348111, 627 | 0.183429, 0.040329, 0.354971, 628 | 0.190367, 0.039309, 0.361447, 629 | 0.197297, 0.038400, 0.367535, 630 | 0.204209, 0.037632, 0.373238, 631 | 0.211095, 0.037030, 0.378563, 632 | 0.217949, 0.036615, 0.383522, 633 | 0.224763, 0.036405, 0.388129, 634 | 0.231538, 0.036405, 0.392400, 635 | 0.238273, 0.036621, 0.396353, 636 | 0.244967, 0.037055, 0.400007, 637 | 0.251620, 0.037705, 0.403378, 638 | 0.258234, 0.038571, 0.406485, 639 | 0.264810, 0.039647, 0.409345, 640 | 0.271347, 0.040922, 0.411976, 641 | 0.277850, 0.042353, 0.414392, 642 | 0.284321, 0.043933, 0.416608, 643 | 0.290763, 0.045644, 0.418637, 644 | 0.297178, 0.047470, 0.420491, 645 | 0.303568, 0.049396, 0.422182, 646 | 0.309935, 0.051407, 0.423721, 647 | 0.316282, 0.053490, 0.425116, 648 | 0.322610, 0.055634, 0.426377, 649 | 0.328921, 0.057827, 0.427511, 650 | 0.335217, 0.060060, 0.428524, 651 | 0.341500, 0.062325, 0.429425, 652 | 0.347771, 0.064616, 0.430217, 653 | 0.354032, 0.066925, 0.430906, 654 | 0.360284, 0.069247, 0.431497, 655 | 0.366529, 0.071579, 0.431994, 656 | 0.372768, 0.073915, 0.432400, 657 | 0.379001, 0.076253, 0.432719, 658 | 0.385228, 0.078591, 0.432955, 659 | 0.391453, 0.080927, 0.433109, 660 | 0.397674, 0.083257, 0.433183, 661 | 0.403894, 0.085580, 0.433179, 662 | 0.410113, 0.087896, 0.433098, 663 | 0.416331, 0.090203, 0.432943, 664 | 0.422549, 0.092501, 0.432714, 665 | 0.428768, 0.094790, 0.432412, 666 | 0.434987, 0.097069, 0.432039, 667 | 0.441207, 0.099338, 0.431594, 668 | 0.447428, 0.101597, 0.431080, 669 | 0.453651, 0.103848, 0.430498, 670 | 0.459875, 0.106089, 0.429846, 671 | 0.466100, 0.108322, 0.429125, 672 | 0.472328, 0.110547, 0.428334, 673 | 0.478558, 0.112764, 0.427475, 674 | 0.484789, 0.114974, 0.426548, 675 | 0.491022, 0.117179, 0.425552, 676 | 0.497257, 0.119379, 0.424488, 677 | 0.503493, 0.121575, 0.423356, 678 | 0.509730, 0.123769, 0.422156, 679 | 0.515967, 0.125960, 0.420887, 680 | 0.522206, 0.128150, 0.419549, 681 | 0.528444, 0.130341, 0.418142, 682 | 0.534683, 0.132534, 0.416667, 683 | 0.540920, 0.134729, 0.415123, 684 | 0.547157, 0.136929, 0.413511, 685 | 0.553392, 0.139134, 0.411829, 686 | 0.559624, 0.141346, 0.410078, 687 | 0.565854, 0.143567, 0.408258, 688 | 0.572081, 0.145797, 0.406369, 689 | 0.578304, 0.148039, 0.404411, 690 | 0.584521, 0.150294, 0.402385, 691 | 0.590734, 0.152563, 0.400290, 692 | 0.596940, 0.154848, 0.398125, 693 | 0.603139, 0.157151, 0.395891, 694 | 0.609330, 0.159474, 0.393589, 695 | 0.615513, 0.161817, 0.391219, 696 | 0.621685, 0.164184, 0.388781, 697 | 0.627847, 0.166575, 0.386276, 698 | 0.633998, 0.168992, 0.383704, 699 | 0.640135, 0.171438, 0.381065, 700 | 0.646260, 0.173914, 0.378359, 701 | 0.652369, 0.176421, 0.375586, 702 | 0.658463, 0.178962, 0.372748, 703 | 0.664540, 0.181539, 0.369846, 704 | 0.670599, 0.184153, 0.366879, 705 | 0.676638, 0.186807, 0.363849, 706 | 0.682656, 0.189501, 0.360757, 707 | 0.688653, 0.192239, 0.357603, 708 | 0.694627, 0.195021, 0.354388, 709 | 0.700576, 0.197851, 0.351113, 710 | 0.706500, 0.200728, 0.347777, 711 | 0.712396, 0.203656, 0.344383, 712 | 0.718264, 0.206636, 0.340931, 713 | 0.724103, 0.209670, 0.337424, 714 | 0.729909, 0.212759, 0.333861, 715 | 0.735683, 0.215906, 0.330245, 716 | 0.741423, 0.219112, 0.326576, 717 | 0.747127, 0.222378, 0.322856, 718 | 0.752794, 0.225706, 0.319085, 719 | 0.758422, 0.229097, 0.315266, 720 | 0.764010, 0.232554, 0.311399, 721 | 0.769556, 0.236077, 0.307485, 722 | 0.775059, 0.239667, 0.303526, 723 | 0.780517, 0.243327, 0.299523, 724 | 0.785929, 0.247056, 0.295477, 725 | 0.791293, 0.250856, 0.291390, 726 | 0.796607, 0.254728, 0.287264, 727 | 0.801871, 0.258674, 0.283099, 728 | 0.807082, 0.262692, 0.278898, 729 | 0.812239, 0.266786, 0.274661, 730 | 0.817341, 0.270954, 0.270390, 731 | 0.822386, 0.275197, 0.266085, 732 | 0.827372, 0.279517, 0.261750, 733 | 0.832299, 0.283913, 0.257383, 734 | 0.837165, 0.288385, 0.252988, 735 | 0.841969, 0.292933, 0.248564, 736 | 0.846709, 0.297559, 0.244113, 737 | 0.851384, 0.302260, 0.239636, 738 | 0.855992, 0.307038, 0.235133, 739 | 0.860533, 0.311892, 0.230606, 740 | 0.865006, 0.316822, 0.226055, 741 | 0.869409, 0.321827, 0.221482, 742 | 0.873741, 0.326906, 0.216886, 743 | 0.878001, 0.332060, 0.212268, 744 | 0.882188, 0.337287, 0.207628, 745 | 0.886302, 0.342586, 0.202968, 746 | 0.890341, 0.347957, 0.198286, 747 | 0.894305, 0.353399, 0.193584, 748 | 0.898192, 0.358911, 0.188860, 749 | 0.902003, 0.364492, 0.184116, 750 | 0.905735, 0.370140, 0.179350, 751 | 0.909390, 0.375856, 0.174563, 752 | 0.912966, 0.381636, 0.169755, 753 | 0.916462, 0.387481, 0.164924, 754 | 0.919879, 0.393389, 0.160070, 755 | 0.923215, 0.399359, 0.155193, 756 | 0.926470, 0.405389, 0.150292, 757 | 0.929644, 0.411479, 0.145367, 758 | 0.932737, 0.417627, 0.140417, 759 | 0.935747, 0.423831, 0.135440, 760 | 0.938675, 0.430091, 0.130438, 761 | 0.941521, 0.436405, 0.125409, 762 | 0.944285, 0.442772, 0.120354, 763 | 0.946965, 0.449191, 0.115272, 764 | 0.949562, 0.455660, 0.110164, 765 | 0.952075, 0.462178, 0.105031, 766 | 0.954506, 0.468744, 0.099874, 767 | 0.956852, 0.475356, 0.094695, 768 | 0.959114, 0.482014, 0.089499, 769 | 0.961293, 0.488716, 0.084289, 770 | 0.963387, 0.495462, 0.079073, 771 | 0.965397, 0.502249, 0.073859, 772 | 0.967322, 0.509078, 0.068659, 773 | 0.969163, 0.515946, 0.063488, 774 | 0.970919, 0.522853, 0.058367, 775 | 0.972590, 0.529798, 0.053324, 776 | 0.974176, 0.536780, 0.048392, 777 | 0.975677, 0.543798, 0.043618, 778 | 0.977092, 0.550850, 0.039050, 779 | 0.978422, 0.557937, 0.034931, 780 | 0.979666, 0.565057, 0.031409, 781 | 0.980824, 0.572209, 0.028508, 782 | 0.981895, 0.579392, 0.026250, 783 | 0.982881, 0.586606, 0.024661, 784 | 0.983779, 0.593849, 0.023770, 785 | 0.984591, 0.601122, 0.023606, 786 | 0.985315, 0.608422, 0.024202, 787 | 0.985952, 0.615750, 0.025592, 788 | 0.986502, 0.623105, 0.027814, 789 | 0.986964, 0.630485, 0.030908, 790 | 0.987337, 0.637890, 0.034916, 791 | 0.987622, 0.645320, 0.039886, 792 | 0.987819, 0.652773, 0.045581, 793 | 0.987926, 0.660250, 0.051750, 794 | 0.987945, 0.667748, 0.058329, 795 | 0.987874, 0.675267, 0.065257, 796 | 0.987714, 0.682807, 0.072489, 797 | 0.987464, 0.690366, 0.079990, 798 | 0.987124, 0.697944, 0.087731, 799 | 0.986694, 0.705540, 0.095694, 800 | 0.986175, 0.713153, 0.103863, 801 | 0.985566, 0.720782, 0.112229, 802 | 0.984865, 0.728427, 0.120785, 803 | 0.984075, 0.736087, 0.129527, 804 | 0.983196, 0.743758, 0.138453, 805 | 0.982228, 0.751442, 0.147565, 806 | 0.981173, 0.759135, 0.156863, 807 | 0.980032, 0.766837, 0.166353, 808 | 0.978806, 0.774545, 0.176037, 809 | 0.977497, 0.782258, 0.185923, 810 | 0.976108, 0.789974, 0.196018, 811 | 0.974638, 0.797692, 0.206332, 812 | 0.973088, 0.805409, 0.216877, 813 | 0.971468, 0.813122, 0.227658, 814 | 0.969783, 0.820825, 0.238686, 815 | 0.968041, 0.828515, 0.249972, 816 | 0.966243, 0.836191, 0.261534, 817 | 0.964394, 0.843848, 0.273391, 818 | 0.962517, 0.851476, 0.285546, 819 | 0.960626, 0.859069, 0.298010, 820 | 0.958720, 0.866624, 0.310820, 821 | 0.956834, 0.874129, 0.323974, 822 | 0.954997, 0.881569, 0.337475, 823 | 0.953215, 0.888942, 0.351369, 824 | 0.951546, 0.896226, 0.365627, 825 | 0.950018, 0.903409, 0.380271, 826 | 0.948683, 0.910473, 0.395289, 827 | 0.947594, 0.917399, 0.410665, 828 | 0.946809, 0.924168, 0.426373, 829 | 0.946392, 0.930761, 0.442367, 830 | 0.946403, 0.937159, 0.458592, 831 | 0.946903, 0.943348, 0.474970, 832 | 0.947937, 0.949318, 0.491426, 833 | 0.949545, 0.955063, 0.507860, 834 | 0.951740, 0.960587, 0.524203, 835 | 0.954529, 0.965896, 0.540361, 836 | 0.957896, 0.971003, 0.556275, 837 | 0.961812, 0.975924, 0.571925, 838 | 0.966249, 0.980678, 0.587206, 839 | 0.971162, 0.985282, 0.602154, 840 | 0.976511, 0.989753, 0.616760, 841 | 0.982257, 0.994109, 0.631017, 842 | 0.988362, 0.998364, 0.644924, 843 | }; 844 | 845 | constexpr double cmap_gray[] = { 846 | 0. , 0. , 0. , 847 | 1. , 1. , 1. 848 | }; 849 | 850 | #endif // COLORMAPS_H 851 | -------------------------------------------------------------------------------- /src/qmlplotting/dataclient.cpp: -------------------------------------------------------------------------------- 1 | #include "dataclient.h" 2 | 3 | DataClient::DataClient(QQuickItem *parent) 4 | : QQuickItem(parent) 5 | , m_new_geometry(false) 6 | , m_new_data(false) 7 | , m_new_source(false) 8 | , m_source(nullptr) 9 | { 10 | 11 | } 12 | 13 | DataClient::~DataClient() = default; 14 | 15 | void DataClient::dataChanged() 16 | { 17 | m_new_data = true; 18 | update(); 19 | } 20 | 21 | void DataClient::setDataSource(QQuickItem *item) 22 | { 23 | auto* d = dynamic_cast(item); 24 | if (d == m_source) { 25 | return; 26 | } 27 | 28 | // disconnect from previous data source 29 | if (m_source != nullptr) { 30 | disconnect(m_source, &DataSource::dataChanged, this, &DataClient::dataChanged); 31 | } 32 | // connect to new data source 33 | if (d != nullptr) { 34 | connect(d, &DataSource::dataChanged, this, &DataClient::dataChanged); 35 | } 36 | m_source = d; 37 | m_new_source = true; 38 | emit dataSourceChanged(d); 39 | update(); 40 | } 41 | 42 | void DataClient::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) 43 | { 44 | QQuickItem::geometryChanged(newGeometry, oldGeometry); 45 | m_new_geometry = true; 46 | update(); 47 | } 48 | -------------------------------------------------------------------------------- /src/qmlplotting/dataclient.h: -------------------------------------------------------------------------------- 1 | #ifndef DATACLIENT_H 2 | #define DATACLIENT_H 3 | 4 | #include 5 | #include "datasource.h" 6 | 7 | class DataClient : public QQuickItem 8 | { 9 | Q_OBJECT 10 | Q_PROPERTY(QQuickItem* dataSource READ dataSource WRITE setDataSource NOTIFY dataSourceChanged) 11 | 12 | signals: 13 | void dataSourceChanged(QQuickItem* item); 14 | 15 | public: 16 | explicit DataClient(QQuickItem *parent = nullptr); 17 | ~DataClient() override; 18 | 19 | QQuickItem* dataSource() const {return m_source;} 20 | virtual void setDataSource(QQuickItem* item); 21 | 22 | protected: 23 | void geometryChanged(const QRectF& newGeometry, const QRectF& oldGeometry) override; 24 | Q_INVOKABLE void dataChanged(); 25 | 26 | bool m_new_geometry; 27 | bool m_new_data; 28 | bool m_new_source; 29 | DataSource* m_source; 30 | }; 31 | 32 | #endif // DATACLIENT_H 33 | -------------------------------------------------------------------------------- /src/qmlplotting/datasource.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "datasource.h" 6 | #include "qsgdatatexture.h" 7 | 8 | #include 9 | 10 | 11 | class DataTexture : public QSGDataTexture 12 | { 13 | public: 14 | DataTexture(const DataSource *source) 15 | : QSGDataTexture() 16 | , m_source(source) 17 | { 18 | } 19 | ~DataTexture() override = default; 20 | 21 | bool updateTexture() override { 22 | QMutexLocker lock(&m_source_access); 23 | if (m_source && m_source->m_new_data) { 24 | // copy/convert data to float texture buffer 25 | float* data = allocateData(m_source->m_dims, m_source->m_num_dims, 1); 26 | int num_elements = 1; 27 | for (int i = 0; i < m_source->m_num_dims; ++i) { 28 | num_elements *= m_source->m_dims[i]; 29 | } 30 | for (int i = 0; i < num_elements; ++i) { 31 | data[i] = m_source->m_data[i]; 32 | } 33 | commitData(); 34 | return true; 35 | } 36 | return false; 37 | } 38 | 39 | QMutex m_source_access; 40 | const DataSource* m_source = nullptr; 41 | }; 42 | 43 | 44 | class DataTextureProvider : public QSGTextureProvider 45 | { 46 | public: 47 | DataTextureProvider(const DataSource *source) 48 | : m_datatexture(new DataTexture(source)) 49 | { 50 | } 51 | 52 | ~DataTextureProvider() override { 53 | delete m_datatexture; 54 | } 55 | 56 | QSGTexture* texture() const override { 57 | return m_datatexture; 58 | } 59 | 60 | DataTexture* m_datatexture; 61 | }; 62 | 63 | 64 | DataSource::DataSource(QQuickItem *parent) 65 | : QQuickItem(parent) 66 | , m_data(nullptr) 67 | , m_num_dims(0) 68 | , m_data_buffer() 69 | , m_new_data(false) 70 | , m_provider(nullptr) 71 | { 72 | for (int& m_dim : m_dims) { 73 | m_dim = 0; 74 | } 75 | } 76 | 77 | DataSource::~DataSource() 78 | { 79 | if (m_provider != nullptr) { 80 | // Synchronously disconnect from texture (find a better way to handle this) 81 | QMutexLocker(&m_provider->m_datatexture->m_source_access); 82 | m_provider->m_datatexture->m_source = nullptr; 83 | m_provider->deleteLater(); 84 | } 85 | } 86 | 87 | bool DataSource::isTextureProvider() const 88 | { 89 | return true; 90 | } 91 | 92 | QSGTextureProvider* DataSource::textureProvider() const 93 | { 94 | if (QOpenGLContext::currentContext() == nullptr) { 95 | qWarning("DataSource::textureProvider needs OpenGL context"); 96 | return nullptr; 97 | } 98 | if (m_provider == nullptr) { 99 | // TODO: use destroyed signal instead of m_provider for cleanup? 100 | const_cast(this)->m_provider = new DataTextureProvider(this); 101 | m_provider->m_datatexture->updateTexture(); 102 | } 103 | return m_provider; 104 | } 105 | 106 | void DataSource::setPointsXY(const QVariantList& points) 107 | { 108 | auto* p_dst = static_cast(allocateData1D(points.size() * 2)); 109 | for (const auto& variant : points) { 110 | const auto p = variant.toPointF(); 111 | *(p_dst++) = p.x(); 112 | *(p_dst++) = p.y(); 113 | } 114 | commitData(); 115 | } 116 | 117 | bool DataSource::copyFloat64Array1D(const QByteArray& data, int size) 118 | { 119 | if (size * static_cast(sizeof(double)) > data.size()) { 120 | return false; 121 | } 122 | auto p_src = reinterpret_cast(data.constData()); 123 | auto p_dst = static_cast(allocateData1D(size)); 124 | for (decltype(size) i = 0; i < size; ++i) { 125 | p_dst[i] = p_src[i]; 126 | } 127 | return commitData(); 128 | } 129 | 130 | bool DataSource::copyFloat64Array2D(const QByteArray& data, int width, int height) 131 | { 132 | if (width * height * static_cast(sizeof(double)) > data.size()) { 133 | return false; 134 | } 135 | auto p_src = reinterpret_cast(data.constData()); 136 | auto p_dst = static_cast(allocateData2D(width, height)); 137 | for (int i = 0; i < width * height; ++i) { 138 | p_dst[i] = p_src[i]; 139 | } 140 | return commitData(); 141 | } 142 | 143 | bool DataSource::setData(double *data, const int *dims, int num_dims) 144 | { 145 | if (num_dims <= 0 || num_dims > 3) { 146 | qWarning("DataSource::setData invalid number of dimensions"); 147 | return false; 148 | } 149 | 150 | bool num_dims_changed = (m_num_dims != num_dims); 151 | bool size_changed = num_dims_changed; 152 | m_num_dims = num_dims; 153 | for (int i = 0; i < 3; ++i) { 154 | if (i < num_dims) { 155 | size_changed = size_changed || (m_dims[i] != dims[i]); 156 | m_dims[i] = dims[i]; 157 | } else { 158 | m_dims[i] = 0; 159 | } 160 | } 161 | m_data = data; 162 | if (num_dims_changed) { 163 | emit dataimensionsChanged(); 164 | } 165 | if (size_changed) { 166 | emit dataSizeChanged(); 167 | } 168 | commitData(); 169 | return true; 170 | } 171 | 172 | bool DataSource::setData1D(void* data, int size) { 173 | return setData(reinterpret_cast(data), &size, 1); 174 | } 175 | 176 | bool DataSource::setData2D(void* data, int width, int height) { 177 | int dims[] = {width, height}; 178 | return setData(reinterpret_cast(data), dims, 2); 179 | } 180 | 181 | bool DataSource::setData3D(void* data, int width, int height, int depth){ 182 | int dims[] = {width, height, depth}; 183 | return setData(reinterpret_cast(data), dims, 3); 184 | } 185 | 186 | void* DataSource::allocateData1D(int size) 187 | { 188 | int num_bytes = size * static_cast(sizeof(double)); 189 | if (m_data_buffer.size() != num_bytes) { 190 | m_data_buffer.resize(num_bytes); 191 | } 192 | auto* data = reinterpret_cast(m_data_buffer.data()); 193 | setData(data, &size, 1); 194 | return data; 195 | } 196 | 197 | void* DataSource::allocateData2D(int width, int height) 198 | { 199 | int num_bytes = width * height * static_cast(sizeof(double)); 200 | if (m_data_buffer.size() != num_bytes) { 201 | m_data_buffer.resize(num_bytes); 202 | } 203 | int dims[] = {width, height}; 204 | auto* data = reinterpret_cast(m_data_buffer.data()); 205 | setData(data, dims, 2); 206 | return data; 207 | } 208 | 209 | void* DataSource::allocateData3D(int width, int height, int depth) 210 | { 211 | int num_bytes = width * height * depth * static_cast(sizeof(double)); 212 | if (m_data_buffer.size() != num_bytes) { 213 | m_data_buffer.resize(num_bytes); 214 | } 215 | int dims[] = {width, height, depth}; 216 | auto* data = reinterpret_cast(m_data_buffer.data()); 217 | setData(data, dims, 3); 218 | return data; 219 | } 220 | 221 | bool DataSource::commitData() 222 | { 223 | m_new_data = true; 224 | emit dataChanged(); 225 | return true; 226 | } 227 | 228 | bool DataSource::ownsData() 229 | { 230 | return m_data == reinterpret_cast(m_data_buffer.data()); 231 | } 232 | 233 | bool DataSource::setTestData1D() 234 | { 235 | int size = 512; 236 | auto* d = reinterpret_cast(allocateData1D(2*size)); 237 | 238 | // gauss + noise 239 | for (int ix = 0; ix < size; ++ix) { 240 | double x = -1. + 2.*(ix * (1./size)); 241 | double r = rand() * (1./RAND_MAX); 242 | d[2*ix+0] = x; 243 | d[2*ix+1] = exp(-(x*x)*5.) * (1. + .2 * (r-.5)); 244 | } 245 | return true; 246 | } 247 | 248 | bool DataSource::setTestData2D() 249 | { 250 | int w = 512, h = 512; 251 | auto* d = reinterpret_cast(allocateData2D(w, h)); 252 | 253 | // gauss + noise 254 | for (int iy = 0; iy < h; ++iy) { 255 | for (int ix = 0; ix < w; ++ix) { 256 | double x = -1. + 2.*(ix * (1./w)); 257 | double y = -1. + 2.*(iy * (1./h)); 258 | double r = rand() * (1./RAND_MAX); 259 | d[iy*w + ix] = exp(-(x*x+y*y)*2.) + .2 * (r-.5); 260 | } 261 | } 262 | // linear strip 263 | for (int iy = h-30; iy < h; ++iy) { 264 | for (int ix = 0; ix < w; ++ix) { 265 | double x = (ix * (1./w)); 266 | d[iy*w + ix] = x; 267 | } 268 | } 269 | return true; 270 | } 271 | -------------------------------------------------------------------------------- /src/qmlplotting/datasource.h: -------------------------------------------------------------------------------- 1 | #ifndef DATASOURCE_H 2 | #define DATASOURCE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class DataTexture; 10 | class DataTextureProvider; 11 | 12 | 13 | class DataSource : public QQuickItem 14 | { 15 | Q_OBJECT 16 | Q_PROPERTY(int dataDimensions MEMBER m_num_dims NOTIFY dataimensionsChanged) 17 | Q_PROPERTY(int dataWidth READ dataWidth NOTIFY dataSizeChanged) 18 | Q_PROPERTY(int dataHeight READ dataHeight NOTIFY dataSizeChanged) 19 | Q_PROPERTY(int dataDepth READ dataDepth NOTIFY dataSizeChanged) 20 | 21 | public: 22 | explicit DataSource(QQuickItem *parent = nullptr); 23 | ~DataSource() override; 24 | 25 | bool isTextureProvider() const override; 26 | QSGTextureProvider *textureProvider() const override; 27 | 28 | int dataWidth() const {return m_dims[0];} 29 | int dataHeight() const {return m_dims[1];} 30 | int dataDepth() const {return m_dims[2];} 31 | 32 | public: 33 | Q_INVOKABLE void setPointsXY(const QVariantList& points); 34 | 35 | public slots: 36 | bool copyFloat64Array1D(const QByteArray& data, int size); 37 | bool copyFloat64Array2D(const QByteArray& data, int width, int height); 38 | bool setTestData1D(); 39 | bool setTestData2D(); 40 | bool setData1D(void* data, int size); 41 | bool setData2D(void* data, int width, int height); 42 | bool setData3D(void* data, int width, int height, int depth); 43 | void* allocateData1D(int size); 44 | void* allocateData2D(int width, int height); 45 | void* allocateData3D(int width, int height, int depth); 46 | void* data() const {return m_data;} 47 | bool commitData(); 48 | bool ownsData(); 49 | 50 | signals: 51 | void dataimensionsChanged(); 52 | void dataSizeChanged(); 53 | void dataChanged(); 54 | 55 | protected: 56 | bool setData(double* data, const int* dims, int num_dims); 57 | 58 | double* m_data; 59 | int m_num_dims; 60 | int m_dims[3]; 61 | QByteArray m_data_buffer; 62 | 63 | private: 64 | bool m_new_data; 65 | DataTextureProvider* m_provider; 66 | friend class DataTexture; 67 | }; 68 | 69 | #endif // DATASOURCE_H 70 | -------------------------------------------------------------------------------- /src/qmlplotting/plotgroup.cpp: -------------------------------------------------------------------------------- 1 | #include "plotgroup.h" 2 | #include 3 | 4 | PlotGroup::PlotGroup(QQuickItem* parent) : QQuickItem(parent) 5 | { 6 | 7 | } 8 | 9 | QQmlListProperty PlotGroup::plotItems() 10 | { 11 | const auto append = [](QQmlListProperty* list, QQuickItem* plotItem) { 12 | auto* self = reinterpret_cast(list->data); 13 | self->m_plotItems.append(plotItem); 14 | // Reparent and anchor to plot group size 15 | plotItem->setParentItem(self); 16 | plotItem->setPosition({0., 0.}); 17 | plotItem->setSize({self->width(), self->height()}); 18 | // Initialize properties from plot group 19 | plotItem->setProperty("viewRect", self->m_viewRect); 20 | plotItem->setProperty("logY", self->m_logY); 21 | }; 22 | const auto count = [](QQmlListProperty* list) -> int { 23 | auto* self = reinterpret_cast(list->data); 24 | return self->m_plotItems.count(); 25 | }; 26 | const auto at = [](QQmlListProperty* list, int index) -> QQuickItem* { 27 | auto* self = reinterpret_cast(list->data); 28 | return self->m_plotItems.at(index); 29 | }; 30 | const auto clear = [](QQmlListProperty* list) { 31 | auto* self = reinterpret_cast(list->data); 32 | self->m_plotItems.clear(); 33 | }; 34 | return {this, this, append, count, at, clear}; 35 | } 36 | 37 | void PlotGroup::setViewMode(const ViewMode viewMode) 38 | { 39 | if (std::exchange(m_viewMode, viewMode) == viewMode) { return; } 40 | updateViewRects(); 41 | emit viewModeChanged(viewMode); 42 | } 43 | 44 | void PlotGroup::setAspectRatio(double aspectRatio) 45 | { 46 | aspectRatio = std::max(aspectRatio, 0.); 47 | if (std::exchange(m_aspectRatio, aspectRatio) == aspectRatio) { return; } 48 | if (m_viewMode != ViewMode::AutoAspect) { updateViewRects(); } 49 | emit aspectRatioChanged(aspectRatio); 50 | } 51 | 52 | void PlotGroup::setViewRect(const QRectF& viewRect) 53 | { 54 | // Set preferred view rect 55 | if (std::exchange(m_preferredViewRect, viewRect) == viewRect) { return; } 56 | // Trigger internal view rect update 57 | updateViewRects(); 58 | } 59 | 60 | void PlotGroup::updateViewRects() 61 | { 62 | // Set actual view rect from preferred, possibly adjusted view rect 63 | const QRectF adjustedViewRect = adjustViewRect(m_preferredViewRect); 64 | if (std::exchange(m_viewRect, adjustedViewRect) == adjustedViewRect) { return; } 65 | // Update all plot items 66 | for (auto& item: m_plotItems) { 67 | item->setProperty("viewRect", m_viewRect); 68 | } 69 | emit viewRectChanged(m_viewRect); 70 | } 71 | 72 | void PlotGroup::setLogY(const bool logY) 73 | { 74 | if (std::exchange(m_logY, logY) == logY) { return; } 75 | // Forward logY change to plot items 76 | for (auto& item: m_plotItems) { 77 | item->setProperty("logY", logY); 78 | } 79 | emit logYChanged(logY); 80 | } 81 | 82 | void PlotGroup::geometryChanged(const QRectF& newGeometry, const QRectF& oldGeometry) 83 | { 84 | QQuickItem::geometryChanged(newGeometry, oldGeometry); 85 | // Resize plot items to new item size 86 | for (auto& item: m_plotItems) { 87 | item->setSize({width(), height()}); 88 | } 89 | // Update view rects if size change triggers aspect adjustment 90 | if (m_viewMode != ViewMode::AutoAspect) { 91 | updateViewRects(); 92 | } 93 | } 94 | 95 | QRectF PlotGroup::adjustViewRect(const QRectF& viewRect) const 96 | { 97 | // TODO: Support aspect != 1 98 | // Aspect correction requires valid item size 99 | if (!(width() > 0 && height() > 0)) { return viewRect; } 100 | // Return unchanged rect in auto mode 101 | if (m_viewMode == ViewMode::AutoAspect) { return viewRect; } 102 | 103 | const double sourceAspect = viewRect.width() / viewRect.height(); 104 | const double targetAspect = width() / height(); 105 | 106 | const auto fitX = [&]() -> QRectF { 107 | const double targetHeight = viewRect.width() / targetAspect; 108 | const double delta = targetHeight - viewRect.height(); 109 | return { viewRect.x(), viewRect.y() - 0.5 * delta, viewRect.width(), targetHeight }; 110 | }; 111 | 112 | const auto fitY = [&]() -> QRectF { 113 | const double targetWidth = viewRect.height() * targetAspect; 114 | const double delta = targetWidth - viewRect.width(); 115 | return { viewRect.x() - 0.5 * delta, viewRect.y(), targetWidth, viewRect.height() }; 116 | }; 117 | 118 | switch (m_viewMode) { 119 | case ViewMode::PreserveAspectFit: 120 | return (targetAspect >= sourceAspect) ? fitY() : fitX(); 121 | case ViewMode::PreserveAspectCrop: 122 | return (targetAspect >= sourceAspect) ? fitX() : fitY(); 123 | default: 124 | return viewRect; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/qmlplotting/plotgroup.h: -------------------------------------------------------------------------------- 1 | #ifndef PLOTGROUP_H 2 | #define PLOTGROUP_H 3 | 4 | #include 5 | 6 | class PlotGroup : public QQuickItem 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | enum class ViewMode { 12 | AutoAspect, 13 | PreserveAspectFit, 14 | PreserveAspectCrop, 15 | }; 16 | Q_ENUM(ViewMode) 17 | 18 | public: 19 | Q_PROPERTY(ViewMode viewMode MEMBER m_viewMode WRITE setViewMode NOTIFY viewModeChanged) 20 | Q_PROPERTY(double aspectRatio MEMBER m_aspectRatio WRITE setAspectRatio NOTIFY aspectRatioChanged) 21 | Q_PROPERTY(QRectF viewRect MEMBER m_viewRect WRITE setViewRect NOTIFY viewRectChanged) 22 | Q_PROPERTY(bool logY MEMBER m_logY WRITE setLogY NOTIFY logYChanged) 23 | Q_PROPERTY(QQmlListProperty plotItems READ plotItems CONSTANT) 24 | 25 | public: 26 | explicit PlotGroup(QQuickItem* parent = nullptr); 27 | 28 | QQmlListProperty plotItems(); 29 | 30 | signals: 31 | void viewModeChanged(ViewMode viewMode); 32 | void aspectRatioChanged(double aspectRatio); 33 | void viewRectChanged(const QRectF& viewRect); 34 | void logYChanged(bool logY); 35 | 36 | public slots: 37 | void setViewMode(ViewMode viewMode); 38 | void setAspectRatio(double aspectRatio); 39 | void setViewRect(const QRectF& viewRect); 40 | void setLogY(bool logY); 41 | 42 | protected: 43 | void geometryChanged(const QRectF& newGeometry, const QRectF& oldGeometry) override; 44 | 45 | private: 46 | ViewMode m_viewMode = ViewMode::AutoAspect; 47 | double m_aspectRatio = 1.0; 48 | QRectF m_viewRect = {0., 0., 1., 1.}; 49 | QRectF m_preferredViewRect = {0., 0., 1., 1.}; 50 | bool m_logY = false; 51 | QVector m_plotItems; 52 | 53 | QRectF adjustViewRect(const QRectF& viewRect) const; 54 | void updateViewRects(); 55 | }; 56 | 57 | #endif // PLOTGROUP_H 58 | -------------------------------------------------------------------------------- /src/qmlplotting/qsgdatatexture.cpp: -------------------------------------------------------------------------------- 1 | #include "qsgdatatexture.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace { 8 | 9 | struct GlTypemap { 10 | const GLint internalFormats[5]; 11 | const GLenum dataType; 12 | }; 13 | 14 | template 15 | struct GlMap 16 | { 17 | static constexpr GLint internalFormat(const int nColors) { 18 | return typemap.internalFormats[nColors]; 19 | } 20 | static constexpr GLenum dataFormat(const int nColors) { 21 | #ifndef QT_OPENGL_ES_2 22 | constexpr GLenum dataFormats[5] = {0, GL_RED, GL_RG, GL_RGB, GL_BGRA}; 23 | #else 24 | constexpr GLenum dataFormats[5] = {0, GL_LUMINANCE, 0, GL_RGB, GL_RGBA}; 25 | #endif 26 | return dataFormats[nColors]; 27 | } 28 | static constexpr GLenum dataType() { return typemap.dataType; } 29 | static const GlTypemap typemap; 30 | }; 31 | 32 | #ifndef QT_OPENGL_ES_2 33 | template<> const GlTypemap GlMap::typemap = { 34 | {0, GL_R32F, GL_RG32F, GL_RGB32F, GL_RGBA32F}, GL_FLOAT 35 | }; 36 | template<> const GlTypemap GlMap::typemap = { 37 | {0, GL_R8, GL_RG8, GL_RGB8, GL_RGBA8}, GL_UNSIGNED_BYTE 38 | }; 39 | #else 40 | template<> const GlTypemap GlMap::typemap = { 41 | {0, GL_LUMINANCE, 0, GL_RGB, GL_RGBA}, GL_FLOAT 42 | }; 43 | template<> const GlTypemap GlMap::typemap = { 44 | {0, GL_LUMINANCE, 0, GL_RGB, GL_RGBA}, GL_UNSIGNED_BYTE 45 | }; 46 | #endif 47 | 48 | } 49 | 50 | template 51 | QSGDataTexture::QSGDataTexture() : 52 | QSGDynamicTexture(), QOpenGLFunctions() 53 | { 54 | initializeOpenGLFunctions(); 55 | } 56 | 57 | template 58 | QSGDataTexture::~QSGDataTexture() { 59 | if (m_id_texture) { 60 | if (QOpenGLContext::currentContext() == nullptr) { 61 | qWarning("QSGDataTexture destroyed without OpenGL context"); 62 | } 63 | glDeleteTextures(1, &m_id_texture); 64 | } 65 | } 66 | 67 | template 68 | int QSGDataTexture::textureId() const { 69 | return static_cast(m_id_texture); 70 | } 71 | 72 | template 73 | QSize QSGDataTexture::textureSize() const { 74 | if (m_num_dims == 2) { 75 | return {m_dims[0], m_dims[1]}; 76 | } 77 | if (m_num_dims == 1) { 78 | return {m_dims[0], 0}; 79 | } 80 | return QSize(); 81 | } 82 | 83 | template 84 | bool QSGDataTexture::hasAlphaChannel() const { 85 | return (m_num_components == 4); 86 | } 87 | 88 | template 89 | bool QSGDataTexture::hasMipmaps() const { 90 | return false; 91 | } 92 | 93 | template 94 | void QSGDataTexture::bind() { 95 | 96 | if (m_id_texture == 0u) { 97 | glGenTextures(1, &m_id_texture); 98 | } 99 | GLint filter = (filtering() == Linear) ? GL_LINEAR : GL_NEAREST; 100 | 101 | switch (m_num_dims) { 102 | case 1: 103 | // Deprecated 104 | break; 105 | case 2: 106 | glBindTexture(GL_TEXTURE_2D, m_id_texture); 107 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); 108 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter); 109 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 110 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 111 | break; 112 | case 3: 113 | #ifndef QT_OPENGL_ES_2 114 | glBindTexture(GL_TEXTURE_3D, m_id_texture); 115 | glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, filter); 116 | glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, filter); 117 | glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 118 | glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 119 | glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); 120 | #endif 121 | break; 122 | default: 123 | break; 124 | } 125 | 126 | if (m_needs_upload) { 127 | m_needs_upload = false; 128 | // determine color format 129 | const GLint internal_format = GlMap::internalFormat(m_num_components); 130 | const GLenum format = GlMap::dataFormat(m_num_components); 131 | const GLenum type = GlMap::dataType(); 132 | 133 | // upload data as 1D or 2D texture 134 | switch (m_num_dims) { 135 | case 1: 136 | // Deprecated 137 | break; 138 | case 2: 139 | glTexImage2D(GL_TEXTURE_2D, 0, internal_format, m_dims[0], m_dims[1], 0, format, type, m_buffer.data()); 140 | break; 141 | case 3: 142 | { 143 | #ifndef QT_OPENGL_ES_2 144 | QOpenGLExtraFunctions* f = QOpenGLContext::currentContext()->extraFunctions(); 145 | f->glTexImage3D(GL_TEXTURE_3D, 0, internal_format, m_dims[0], m_dims[1], m_dims[2], 0, format, type, m_buffer.data()); 146 | #endif 147 | break; 148 | } 149 | default: 150 | return; 151 | } 152 | } 153 | } 154 | 155 | template 156 | T *QSGDataTexture::allocateData(const int *dims, int num_dims, int num_components) 157 | { 158 | if (num_components < 1 || num_components > 4) { 159 | return nullptr; 160 | } 161 | if (num_dims < 1 || num_dims > 3) { 162 | return nullptr; 163 | } 164 | 165 | // calculate total number of elements 166 | int num_elements = num_components; 167 | for (int i = 0; i < num_dims; ++i) { 168 | m_dims[i] = dims[i]; 169 | num_elements *= dims[i]; 170 | } 171 | 172 | // calculate total number of bytes, resize buffer if required 173 | int num_bytes = num_elements * static_cast(sizeof(T)); 174 | if (m_buffer.size() != num_bytes) { 175 | m_buffer.resize(num_bytes); 176 | } 177 | 178 | m_num_dims = num_dims; 179 | m_num_components = num_components; 180 | return reinterpret_cast(m_buffer.data()); 181 | } 182 | 183 | template 184 | T* QSGDataTexture::allocateData2D(int width, int height, int num_components) 185 | { 186 | int dims[] = {width, height}; 187 | return reinterpret_cast(allocateData(dims, 2, num_components)); 188 | } 189 | 190 | template 191 | T* QSGDataTexture::allocateData3D(int width, int height, int depth, int num_components) 192 | { 193 | int dims[] = {width, height, depth}; 194 | return reinterpret_cast(allocateData(dims, 3, num_components)); 195 | } 196 | 197 | template 198 | void QSGDataTexture::commitData() 199 | { 200 | m_needs_upload = true; 201 | } 202 | 203 | template 204 | int QSGDataTexture::getDim(int dim) 205 | { 206 | return m_dims[dim]; 207 | } 208 | 209 | template 210 | bool QSGDataTexture::updateTexture() 211 | { 212 | return false; 213 | } 214 | 215 | 216 | // Explicitly instantiate data texture classes in this unit 217 | template class QSGDataTexture; 218 | template class QSGDataTexture; 219 | -------------------------------------------------------------------------------- /src/qmlplotting/qsgdatatexture.h: -------------------------------------------------------------------------------- 1 | #ifndef QSGDATATEXTURE_H 2 | #define QSGDATATEXTURE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | template 10 | class QSGDataTexture : public QSGDynamicTexture, protected QOpenGLFunctions 11 | { 12 | public: 13 | QSGDataTexture(); 14 | ~QSGDataTexture() override; 15 | 16 | int textureId() const override; 17 | QSize textureSize() const override; 18 | bool hasAlphaChannel() const override; 19 | bool hasMipmaps() const override; 20 | void bind() override; 21 | 22 | T* allocateData(const int *dims, int num_dims, int num_components); 23 | T* allocateData2D(int width, int height, int num_components); 24 | T* allocateData3D(int width, int height, int depth, int num_components); 25 | void commitData(); 26 | 27 | int getDim(int dim); 28 | 29 | bool updateTexture() override; 30 | 31 | private: 32 | unsigned int m_id_texture = 0; 33 | int m_num_dims = 0; 34 | int m_dims[3] = {0, 0, 0}; 35 | int m_num_components = 0; 36 | QByteArray m_buffer; 37 | bool m_needs_upload = false; 38 | }; 39 | 40 | extern template class QSGDataTexture; 41 | extern template class QSGDataTexture; 42 | 43 | #endif // QSGDATATEXTURE_H 44 | -------------------------------------------------------------------------------- /src/qmlplotting/sliceplot.cpp: -------------------------------------------------------------------------------- 1 | #include "sliceplot.h" 2 | 3 | #include 4 | #include 5 | 6 | #define GLSL(ver, src) "#version " #ver "\n" #src 7 | 8 | class SlicePlotMaterial : public QSGMaterial 9 | { 10 | public: 11 | SlicePlotMaterial(bool filled) : m_filled(filled) {} 12 | ~SlicePlotMaterial() override = default; 13 | QSGTexture* m_texture_data; 14 | double m_width; 15 | double m_height; 16 | double m_amplitude; 17 | double m_offset; 18 | QPointF m_p1; 19 | QPointF m_p2; 20 | QColor m_color; 21 | const bool m_filled; 22 | }; 23 | 24 | class SliceLinePlotMaterial : public SlicePlotMaterial 25 | { 26 | public: 27 | SliceLinePlotMaterial() : SlicePlotMaterial(false) {} 28 | ~SliceLinePlotMaterial() override = default; 29 | 30 | QSGMaterialType *type() const override { 31 | static QSGMaterialType type; 32 | return &type; 33 | } 34 | QSGMaterialShader *createShader() const override; 35 | }; 36 | 37 | class SliceFillPlotMaterial : public SlicePlotMaterial 38 | { 39 | public: 40 | SliceFillPlotMaterial() : SlicePlotMaterial(true) { 41 | setFlag(QSGMaterial::Blending); 42 | } 43 | ~SliceFillPlotMaterial() override = default; 44 | 45 | QSGMaterialType *type() const override { 46 | static QSGMaterialType type; 47 | return &type; 48 | } 49 | QSGMaterialShader *createShader() const override; 50 | }; 51 | 52 | class SlicePlotShader : public QSGMaterialShader 53 | { 54 | public: 55 | SlicePlotShader() = default; 56 | ~SlicePlotShader() override = default; 57 | 58 | char const *const *attributeNames() const override { 59 | static char const *const names[] = { "vertex", nullptr }; 60 | return names; 61 | } 62 | 63 | void initialize() override { 64 | QSGMaterialShader::initialize(); 65 | m_id_matrix = program()->uniformLocation("matrix"); 66 | m_id_opacity = program()->uniformLocation("opacity"); 67 | m_id_color = program()->uniformLocation("color"); 68 | m_id_data = program()->uniformLocation("image"); 69 | m_id_width = program()->uniformLocation("width"); 70 | m_id_height = program()->uniformLocation("height"); 71 | m_id_amplitude = program()->uniformLocation("amplitude"); 72 | m_id_offset = program()->uniformLocation("offset"); 73 | m_id_p1 = program()->uniformLocation("p1"); 74 | m_id_p2 = program()->uniformLocation("p2"); 75 | } 76 | 77 | void activate() override { 78 | } 79 | 80 | void updateState(const RenderState& state, QSGMaterial *newMaterial, QSGMaterial *) override { 81 | Q_ASSERT(program()->isLinked()); 82 | auto* material = static_cast(newMaterial); 83 | 84 | if (state.isMatrixDirty()) { 85 | program()->setUniformValue(m_id_matrix, state.combinedMatrix()); 86 | } 87 | if (state.isOpacityDirty()) { 88 | program()->setUniformValue(m_id_opacity, state.opacity()); 89 | } 90 | 91 | // bind material parameters 92 | program()->setUniformValue(m_id_width, float(material->m_width)); 93 | program()->setUniformValue(m_id_height, float(material->m_height)); 94 | program()->setUniformValue(m_id_amplitude, float(material->m_amplitude)); 95 | program()->setUniformValue(m_id_offset, float(material->m_offset)); 96 | program()->setUniformValue(m_id_p1, material->m_p1); 97 | program()->setUniformValue(m_id_p2, material->m_p2); 98 | program()->setUniformValue(m_id_color, material->m_color); 99 | 100 | // bind the material data texture 101 | program()->setUniformValue(m_id_data, 0); 102 | material->m_texture_data->bind(); 103 | } 104 | 105 | void deactivate() override { 106 | } 107 | 108 | protected: 109 | int m_id_matrix; 110 | int m_id_opacity; 111 | int m_id_color; 112 | int m_id_width; 113 | int m_id_height; 114 | int m_id_data; 115 | int m_id_amplitude; 116 | int m_id_offset; 117 | int m_id_p1; 118 | int m_id_p2; 119 | }; 120 | 121 | class SliceLinePlotShader : public SlicePlotShader 122 | { 123 | public: 124 | SliceLinePlotShader() = default; 125 | ~SliceLinePlotShader() override = default; 126 | 127 | const char *vertexShader() const override { 128 | return GLSL(100, 129 | attribute highp vec2 vertex; 130 | uniform sampler2D data; 131 | uniform highp float width; 132 | uniform highp float height; 133 | uniform highp float amplitude; 134 | uniform highp float offset; 135 | uniform highp vec2 p1; 136 | uniform highp vec2 p2; 137 | uniform highp mat4 matrix; 138 | 139 | void main() { 140 | highp vec2 pos = (1.-vertex.x)*p1 + vertex.x*p2; 141 | highp float val = texture2D(data, pos).r; 142 | highp float yval = amplitude * (val+offset); 143 | gl_Position = matrix * vec4(width * vertex.x, height * (1.-yval), 0., 1.); 144 | } 145 | ); 146 | } 147 | 148 | const char *fragmentShader() const override { 149 | return R"( 150 | uniform sampler2D data; 151 | uniform lowp float opacity; 152 | uniform mediump vec4 color; 153 | 154 | void main() { 155 | lowp float o = opacity * color.a; 156 | gl_FragColor.rgb = color.rgb * o; 157 | gl_FragColor.a = o; 158 | } 159 | )"; 160 | } 161 | }; 162 | 163 | class SliceFillPlotShader : public SlicePlotShader 164 | { 165 | public: 166 | SliceFillPlotShader() = default; 167 | ~SliceFillPlotShader() override = default; 168 | 169 | const char *vertexShader() const override { 170 | return GLSL(100, 171 | attribute highp vec2 vertex; 172 | uniform sampler2D data; 173 | uniform highp float width; 174 | uniform highp float height; 175 | uniform highp vec2 p1; 176 | uniform highp vec2 p2; 177 | uniform highp mat4 matrix; 178 | varying highp vec2 pos; 179 | varying highp vec2 coord; 180 | 181 | void main() { 182 | coord = (1.-vertex.x)*p1 + vertex.x*p2; 183 | pos = vec2(vertex.x, 1.-vertex.y); 184 | gl_Position = matrix * vec4(width * vertex.x, height * vertex.y, 0., 1.); 185 | } 186 | ); 187 | } 188 | 189 | const char *fragmentShader() const override { 190 | return R"( 191 | uniform sampler2D data; 192 | uniform lowp float opacity; 193 | uniform highp float amplitude; 194 | uniform highp float offset; 195 | uniform mediump vec4 color; 196 | varying highp vec2 pos; 197 | varying highp vec2 coord; 198 | 199 | void main() { 200 | highp float pos_y = pos.y - amplitude*offset; 201 | highp float y_val = amplitude * texture2D(data, coord).r; 202 | highp float dist_above_curve = sign(y_val) * (pos_y-y_val); 203 | float below_curve = float(dist_above_curve < 0.); 204 | float above_zero = float(sign(y_val) * pos_y >= 0.); 205 | float fill = below_curve * above_zero; 206 | lowp float o = opacity * color.a * fill; 207 | gl_FragColor.rgb = color.rgb * o; 208 | gl_FragColor.a = o; 209 | } 210 | )"; 211 | } 212 | }; 213 | 214 | inline QSGMaterialShader* SliceLinePlotMaterial::createShader() const { return new SliceLinePlotShader; } 215 | inline QSGMaterialShader* SliceFillPlotMaterial::createShader() const { return new SliceFillPlotShader; } 216 | 217 | // ---------------------------------------------------------------------------- 218 | 219 | 220 | SlicePlot::SlicePlot(QQuickItem *parent) : 221 | DataClient(parent), 222 | m_min_value(0.), m_max_value(1.), m_num_segments(20), 223 | m_p1(0., 0.), m_p2(1., 1.), 224 | m_color(Qt::red), m_filled(false) 225 | { 226 | setFlag(QQuickItem::ItemHasContents); 227 | setClip(true); 228 | } 229 | 230 | SlicePlot::~SlicePlot() = default; 231 | 232 | void SlicePlot::setMinimumValue(double value) 233 | { 234 | if (value == m_min_value) { 235 | return; 236 | } 237 | m_min_value = value; 238 | emit minimumValueChanged(value); 239 | update(); 240 | } 241 | 242 | void SlicePlot::setMaximumValue(double value) 243 | { 244 | if (value == m_max_value) { 245 | return; 246 | } 247 | m_max_value = value; 248 | emit maximumValueChanged(value); 249 | update(); 250 | } 251 | 252 | void SlicePlot::setNumSegments(int n) 253 | { 254 | if (n == m_num_segments) { 255 | return; 256 | } 257 | m_num_segments = n; 258 | m_new_geometry = true; 259 | emit numSegmentsChanged(n); 260 | update(); 261 | } 262 | 263 | void SlicePlot::setP1(const QPointF &p) 264 | { 265 | if (m_p1 == p) { 266 | return; 267 | } 268 | m_p1 = p; 269 | emit p1Changed(m_p1); 270 | update(); 271 | } 272 | 273 | void SlicePlot::setP2(const QPointF &p) 274 | { 275 | if (m_p2 == p) { 276 | return; 277 | } 278 | m_p2 = p; 279 | emit p2Changed(m_p2); 280 | update(); 281 | } 282 | 283 | void SlicePlot::setColor(const QColor &color) 284 | { 285 | if (m_color == color) { 286 | return; 287 | } 288 | m_color = color; 289 | emit colorChanged(m_color); 290 | update(); 291 | } 292 | 293 | void SlicePlot::setFilled(bool filled) 294 | { 295 | if (m_filled == filled) { 296 | return; 297 | } 298 | m_filled = filled; 299 | emit filledChanged(m_filled); 300 | update(); 301 | } 302 | 303 | QSGNode *SlicePlot::updatePaintNode(QSGNode *n, QQuickItem::UpdatePaintNodeData *) 304 | { 305 | QSGGeometryNode* n_geom; 306 | QSGGeometry *geometry; 307 | SlicePlotMaterial *material; 308 | 309 | if (n == nullptr) { 310 | n = new QSGNode; 311 | } 312 | 313 | if (m_source == nullptr) { 314 | // remove child node if there is no data source 315 | if (n->firstChild() != nullptr) { 316 | n_geom = static_cast(n->firstChild()); 317 | n->removeAllChildNodes(); 318 | delete n_geom; 319 | } 320 | // return empty node 321 | return n; 322 | } 323 | 324 | if (n->firstChild() == nullptr) { 325 | // create child node if there is a data source 326 | n_geom = new QSGGeometryNode(); 327 | n_geom->setFlag(QSGNode::OwnedByParent); 328 | // inintialize geometry & material 329 | if (m_filled) { 330 | geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0); 331 | geometry->setDrawingMode(GL_TRIANGLE_STRIP); 332 | material = new SliceFillPlotMaterial; 333 | material->m_texture_data = m_source->textureProvider()->texture(); 334 | } else { 335 | geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0); 336 | geometry->setDrawingMode(GL_LINE_STRIP); 337 | geometry->setLineWidth(1); 338 | material = new SliceLinePlotMaterial; 339 | material->m_texture_data = m_source->textureProvider()->texture(); 340 | } 341 | n_geom->setGeometry(geometry); 342 | n_geom->setFlag(QSGNode::OwnsGeometry); 343 | n_geom->setMaterial(material); 344 | n_geom->setFlag(QSGNode::OwnsMaterial); 345 | // 346 | n->appendChildNode(n_geom); 347 | } 348 | 349 | // ** graph node and data source can be considered valid from here on ** 350 | 351 | n_geom = static_cast(n->firstChild()); 352 | geometry = n_geom->geometry(); 353 | material = static_cast(n_geom->material()); 354 | QSGNode::DirtyState dirty_state = QSGNode::DirtyMaterial; 355 | 356 | // check if the filled parameter was changed, switch material if necessary 357 | if (m_filled != material->m_filled) { 358 | if (m_filled) { 359 | geometry->allocate(0); 360 | geometry->setDrawingMode(GL_TRIANGLE_STRIP); 361 | material = new SliceFillPlotMaterial; 362 | material->m_texture_data = m_source->textureProvider()->texture(); 363 | } else { 364 | geometry->allocate(0); 365 | geometry->setDrawingMode(GL_LINE_STRIP); 366 | geometry->setLineWidth(1.); 367 | material = new SliceLinePlotMaterial; 368 | material->m_texture_data = m_source->textureProvider()->texture(); 369 | } 370 | n_geom->setMaterial(material); 371 | } 372 | 373 | // update material parameters 374 | material->m_width = width(); 375 | material->m_height = height(); 376 | 377 | //material->m_amplitude = - scale_val; 378 | //material->m_offset = off_val - 1./scale_val; 379 | material->m_amplitude = 1. / (m_max_value - m_min_value); 380 | material->m_offset = -m_min_value; 381 | 382 | material->m_p1 = m_p1; 383 | material->m_p2 = m_p2; 384 | material->m_color = m_color; 385 | 386 | // update geometry if the number of segments changed 387 | if (m_filled) { 388 | if (geometry->vertexCount() != 4) { 389 | geometry->allocate(4); 390 | geometry->vertexDataAsPoint2D()[0].set(0, 0); 391 | geometry->vertexDataAsPoint2D()[1].set(0, 1); 392 | geometry->vertexDataAsPoint2D()[2].set(1, 0); 393 | geometry->vertexDataAsPoint2D()[3].set(1, 1); 394 | dirty_state |= QSGNode::DirtyGeometry; 395 | } 396 | } else { 397 | if (geometry->vertexCount() != (m_num_segments+1)) { 398 | geometry->allocate(m_num_segments+1); 399 | auto* data = static_cast(geometry->vertexData()); 400 | for (int i = 0; i <= m_num_segments; ++i) { 401 | double f = i * (1./m_num_segments); 402 | data[2*i] = static_cast(f); 403 | } 404 | dirty_state |= QSGNode::DirtyGeometry; 405 | } 406 | } 407 | 408 | // check for data source change 409 | if (m_new_source) { 410 | material->m_texture_data = m_source->textureProvider()->texture(); 411 | m_new_source = false; 412 | } 413 | 414 | // check if the image texture should be updated 415 | if (m_new_data) { 416 | static_cast(material->m_texture_data)->updateTexture(); 417 | m_new_data = false; 418 | } 419 | 420 | n->markDirty(dirty_state); 421 | n_geom->markDirty(dirty_state); 422 | return n; 423 | } 424 | -------------------------------------------------------------------------------- /src/qmlplotting/sliceplot.h: -------------------------------------------------------------------------------- 1 | #ifndef LINEPLOT_H 2 | #define LINEPLOT_H 3 | 4 | #include 5 | #include "dataclient.h" 6 | 7 | class SlicePlot : public DataClient 8 | { 9 | Q_OBJECT 10 | Q_PROPERTY(double minimumValue READ minimumValue WRITE setMinimumValue NOTIFY minimumValueChanged) 11 | Q_PROPERTY(double maximumValue READ maximumValue WRITE setMaximumValue NOTIFY maximumValueChanged) 12 | Q_PROPERTY(int numSegments READ numSegments WRITE setNumSegments NOTIFY numSegmentsChanged) 13 | Q_PROPERTY(QPointF p1 READ p1 WRITE setP1 NOTIFY p1Changed) 14 | Q_PROPERTY(QPointF p2 READ p2 WRITE setP2 NOTIFY p2Changed) 15 | Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) 16 | Q_PROPERTY(bool filled MEMBER m_filled WRITE setFilled NOTIFY filledChanged) 17 | 18 | public: 19 | explicit SlicePlot(QQuickItem *parent = nullptr); 20 | ~SlicePlot() override; 21 | 22 | double minimumValue() const {return m_min_value;} 23 | double maximumValue() const {return m_max_value;} 24 | int numSegments() const {return m_num_segments;} 25 | const QPointF& p1() const {return m_p1;} 26 | const QPointF& p2() const {return m_p2;} 27 | const QColor& color() const {return m_color;} 28 | 29 | void setMinimumValue(double value); 30 | void setMaximumValue(double value); 31 | void setNumSegments(int n); 32 | void setP1(const QPointF& p); 33 | void setP2(const QPointF& p); 34 | void setColor(const QColor& color); 35 | void setFilled(bool filled); 36 | 37 | signals: 38 | void minimumValueChanged(double); 39 | void maximumValueChanged(double); 40 | void numSegmentsChanged(int); 41 | void p1Changed(const QPointF&); 42 | void p2Changed(const QPointF&); 43 | void colorChanged(const QColor& color); 44 | void filledChanged(bool); 45 | 46 | protected: 47 | QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* updatePaintNodeData) override; 48 | 49 | private: 50 | double m_min_value; 51 | double m_max_value; 52 | int m_num_segments; 53 | QPointF m_p1; 54 | QPointF m_p2; 55 | QColor m_color; 56 | bool m_filled; 57 | }; 58 | 59 | #endif // LINEPLOT_H 60 | -------------------------------------------------------------------------------- /src/qmlplotting/xyplot.cpp: -------------------------------------------------------------------------------- 1 | #include "xyplot.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "qsgdatatexture.h" 12 | 13 | #ifndef M_PI 14 | #define M_PI 3.14159265358979323846 15 | #endif 16 | 17 | #define GLSL(ver, src) "#version " #ver "\n" #src 18 | 19 | 20 | class XYMarkerMaterial : public QSGMaterial 21 | { 22 | public: 23 | XYMarkerMaterial() = default; 24 | QSGMaterialType *type() const override { 25 | static QSGMaterialType type; 26 | return &type; 27 | } 28 | QSGMaterialShader *createShader() const override; 29 | QSizeF m_size; 30 | QSizeF m_scale; 31 | QPointF m_offset; 32 | QColor m_markercolor; 33 | double m_markersize = 0; 34 | int m_markersegments = 0; 35 | bool m_markerborder = false; 36 | QSGDataTexture m_markerimage; 37 | }; 38 | 39 | class XYMarkerMaterialShader : public QSGMaterialShader 40 | { 41 | public: 42 | const char *vertexShader() const override { 43 | return GLSL(100, 44 | attribute highp vec4 vertex; 45 | uniform highp mat4 matrix; 46 | uniform highp vec2 size; 47 | uniform highp vec2 scale; 48 | uniform highp vec2 offset; 49 | uniform float msize; 50 | 51 | void main() { 52 | highp vec2 p = (vertex.xy - offset) * scale * size; 53 | gl_Position = matrix * vec4(p.x, size.y - p.y, 0., 1.); 54 | gl_PointSize = msize; 55 | } 56 | ); 57 | } 58 | 59 | const char *fragmentShader() const override { 60 | return GLSL(120, 61 | uniform lowp float opacity; 62 | uniform lowp vec4 mcolor; 63 | uniform sampler2D mimage; 64 | 65 | void main() { 66 | lowp vec4 color = mcolor * texture2D(mimage, gl_PointCoord.xy); 67 | lowp float o = opacity * color.a; 68 | gl_FragColor = vec4(color.rgb * o, o); 69 | } 70 | ); 71 | } 72 | 73 | char const *const *attributeNames() const override { 74 | static char const *const names[] = { "vertex", nullptr }; 75 | return names; 76 | } 77 | 78 | void initialize() override { 79 | QSGMaterialShader::initialize(); 80 | QOpenGLShaderProgram* p = program(); 81 | 82 | m_id_matrix = p->uniformLocation("matrix"); 83 | m_id_opacity = p->uniformLocation("opacity"); 84 | m_id_size = p->uniformLocation("size"); 85 | m_id_scale = p->uniformLocation("scale"); 86 | m_id_offset = p->uniformLocation("offset"); 87 | m_id_msize = p->uniformLocation("msize"); 88 | m_id_mcolor = p->uniformLocation("mcolor"); 89 | m_id_mimage = p->uniformLocation("mimage"); 90 | } 91 | 92 | void activate() override { 93 | QOpenGLFunctions *glFuncs = QOpenGLContext::currentContext()->functions(); 94 | #ifndef QT_OPENGL_ES_2 95 | glFuncs->glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); 96 | glFuncs->glEnable(GL_POINT_SPRITE); // TODO: this is deprecated, but appears to be required on NVidia systems 97 | #endif 98 | } 99 | 100 | void updateState(const RenderState& state, QSGMaterial* newMaterial, QSGMaterial*) override { 101 | Q_ASSERT(program()->isLinked()); 102 | auto* material = static_cast(newMaterial); 103 | QOpenGLShaderProgram* p = program(); 104 | 105 | if (state.isMatrixDirty()) { 106 | program()->setUniformValue(m_id_matrix, state.combinedMatrix()); 107 | } 108 | if (state.isOpacityDirty()) { 109 | program()->setUniformValue(m_id_opacity, state.opacity()); 110 | } 111 | 112 | // bind material parameters 113 | p->setUniformValue(m_id_size, material->m_size); 114 | p->setUniformValue(m_id_scale, material->m_scale); 115 | p->setUniformValue(m_id_offset, material->m_offset); 116 | p->setUniformValue(m_id_msize, float(material->m_markersize)); 117 | p->setUniformValue(m_id_mcolor, material->m_markercolor); 118 | 119 | // bind texture 120 | p->setUniformValue(m_id_mimage, 0); 121 | material->m_markerimage.bind(); 122 | } 123 | 124 | private: 125 | int m_id_matrix; 126 | int m_id_opacity; 127 | int m_id_size; 128 | int m_id_scale; 129 | int m_id_offset; 130 | int m_id_msize; 131 | int m_id_mcolor; 132 | int m_id_mimage; 133 | }; 134 | 135 | inline QSGMaterialShader* XYMarkerMaterial::createShader() const { return new XYMarkerMaterialShader; } 136 | 137 | 138 | class XYLineMaterial : public QSGMaterial 139 | { 140 | public: 141 | QSGMaterialType* type() const override { 142 | static QSGMaterialType type; 143 | return &type; 144 | } 145 | QSGMaterialShader* createShader() const override; 146 | QSizeF m_size; 147 | QSizeF m_scale; 148 | QPointF m_offset; 149 | QColor m_color; 150 | }; 151 | 152 | class XYLineMaterialShader : public QSGMaterialShader 153 | { 154 | public: 155 | const char* vertexShader() const override { 156 | return GLSL(100, 157 | attribute highp vec4 vertex; 158 | uniform highp mat4 matrix; 159 | uniform highp vec2 size; 160 | uniform highp vec2 scale; 161 | uniform highp vec2 offset; 162 | 163 | void main() { 164 | highp vec2 p = (vertex.xy - offset) * scale * size; 165 | gl_Position = matrix * vec4(p.x, size.y - p.y, 0., 1.); 166 | } 167 | ); 168 | } 169 | 170 | const char* fragmentShader() const override { 171 | return R"( 172 | uniform lowp vec4 color; 173 | uniform lowp float opacity; 174 | 175 | void main() { 176 | gl_FragColor = vec4(color.rgb*color.a, color.a) * opacity; 177 | } 178 | )"; 179 | } 180 | 181 | char const *const *attributeNames() const override { 182 | static char const *const names[] = {"vertex", nullptr}; 183 | return names; 184 | } 185 | 186 | void initialize() override { 187 | QSGMaterialShader::initialize(); 188 | QOpenGLShaderProgram* p = program(); 189 | 190 | m_id_matrix = p->uniformLocation("matrix"); 191 | m_id_opacity = p->uniformLocation("opacity"); 192 | m_id_size = p->uniformLocation("size"); 193 | m_id_scale = p->uniformLocation("scale"); 194 | m_id_offset = p->uniformLocation("offset"); 195 | m_id_color = p->uniformLocation("color"); 196 | } 197 | 198 | void updateState(const RenderState& state, QSGMaterial* newMaterial, QSGMaterial*) override { 199 | Q_ASSERT(program()->isLinked()); 200 | auto* material = static_cast(newMaterial); 201 | QOpenGLShaderProgram* p = program(); 202 | 203 | if (state.isMatrixDirty()) { 204 | program()->setUniformValue(m_id_matrix, state.combinedMatrix()); 205 | } 206 | if (state.isOpacityDirty()) { 207 | program()->setUniformValue(m_id_opacity, state.opacity()); 208 | } 209 | 210 | // bind material parameters 211 | p->setUniformValue(m_id_size, material->m_size); 212 | p->setUniformValue(m_id_scale, material->m_scale); 213 | p->setUniformValue(m_id_offset, material->m_offset); 214 | p->setUniformValue(m_id_color, material->m_color); 215 | } 216 | 217 | private: 218 | int m_id_matrix; 219 | int m_id_opacity; 220 | int m_id_size; 221 | int m_id_scale; 222 | int m_id_offset; 223 | int m_id_color; 224 | }; 225 | 226 | inline QSGMaterialShader* XYLineMaterial::createShader() const { return new XYLineMaterialShader; } 227 | 228 | 229 | class XYFillMaterial : public QSGMaterial 230 | { 231 | public: 232 | QSGMaterialType* type() const override { 233 | static QSGMaterialType type; 234 | return &type; 235 | } 236 | QSGMaterialShader* createShader() const override; 237 | QSizeF m_size; 238 | QSizeF m_scale; 239 | QPointF m_offset; 240 | QColor m_color; 241 | }; 242 | 243 | class XYFillMaterialShader : public QSGMaterialShader 244 | { 245 | public: 246 | const char* vertexShader() const override { 247 | return GLSL(100, 248 | attribute highp vec4 vertex; 249 | uniform highp mat4 matrix; 250 | uniform highp vec2 size; 251 | uniform highp vec2 scale; 252 | uniform highp vec2 offset; 253 | 254 | void main() { 255 | highp vec2 p = (vertex.xy - offset) * scale * size; 256 | gl_Position = matrix * vec4(p.x, size.y - p.y, 0., 1.); 257 | } 258 | ); 259 | } 260 | 261 | const char* fragmentShader() const override { 262 | return R"( 263 | uniform lowp vec4 color; 264 | uniform lowp float opacity; 265 | 266 | void main() { 267 | gl_FragColor = vec4(color.rgb*color.a, color.a) * opacity; 268 | } 269 | )"; 270 | } 271 | 272 | char const *const *attributeNames() const override { 273 | static char const *const names[] = {"vertex", nullptr}; 274 | return names; 275 | } 276 | 277 | void initialize() override { 278 | QSGMaterialShader::initialize(); 279 | QOpenGLShaderProgram* p = program(); 280 | 281 | m_id_matrix = p->uniformLocation("matrix"); 282 | m_id_opacity = p->uniformLocation("opacity"); 283 | m_id_size = p->uniformLocation("size"); 284 | m_id_scale = p->uniformLocation("scale"); 285 | m_id_offset = p->uniformLocation("offset"); 286 | m_id_color = p->uniformLocation("color"); 287 | } 288 | 289 | void updateState(const RenderState &state, QSGMaterial *newMaterial, QSGMaterial *) override { 290 | Q_ASSERT(program()->isLinked()); 291 | auto* material = static_cast(newMaterial); 292 | QOpenGLShaderProgram* p = program(); 293 | 294 | if (state.isMatrixDirty()) { 295 | program()->setUniformValue(m_id_matrix, state.combinedMatrix()); 296 | } 297 | if (state.isOpacityDirty()) { 298 | program()->setUniformValue(m_id_opacity, state.opacity()); 299 | } 300 | 301 | // bind material parameters 302 | p->setUniformValue(m_id_size, material->m_size); 303 | p->setUniformValue(m_id_scale, material->m_scale); 304 | p->setUniformValue(m_id_offset, material->m_offset); 305 | p->setUniformValue(m_id_color, material->m_color); 306 | } 307 | 308 | private: 309 | int m_id_matrix; 310 | int m_id_opacity; 311 | int m_id_size; 312 | int m_id_scale; 313 | int m_id_offset; 314 | int m_id_color; 315 | }; 316 | 317 | inline QSGMaterialShader* XYFillMaterial::createShader() const { return new XYFillMaterialShader; } 318 | 319 | 320 | XYPlot::XYPlot(QQuickItem *parent) : DataClient(parent) 321 | { 322 | setFlag(QQuickItem::ItemHasContents); 323 | setClip(true); 324 | } 325 | 326 | void XYPlot::setViewRect(const QRectF &viewrect) 327 | { 328 | if (viewrect != m_view_rect) { 329 | m_view_rect = viewrect; 330 | emit viewRectChanged(m_view_rect); 331 | update(); 332 | } 333 | } 334 | 335 | void XYPlot::setFillEnabled(bool enabled) 336 | { 337 | if (m_fill != enabled) { 338 | m_fill = enabled; 339 | emit fillEnabledChanged(m_fill); 340 | update(); 341 | } 342 | } 343 | 344 | void XYPlot::setFillColor(const QColor &color) 345 | { 346 | if (m_fillcolor != color) { 347 | m_fillcolor = color; 348 | emit fillColorChanged(m_fillcolor); 349 | update(); 350 | } 351 | } 352 | 353 | void XYPlot::setLineEnabled(bool enabled) 354 | { 355 | if (m_line != enabled) { 356 | m_line = enabled; 357 | emit lineEnabledChanged(m_line); 358 | update(); 359 | } 360 | } 361 | 362 | void XYPlot::setLineWidth(double width) 363 | { 364 | if (m_linewidth != width) { 365 | m_linewidth = width; 366 | emit lineWidthChanged(m_linewidth); 367 | update(); 368 | } 369 | } 370 | 371 | void XYPlot::setLineColor(const QColor &color) 372 | { 373 | if (m_linecolor != color) { 374 | m_linecolor = color; 375 | emit lineColorChanged(m_linecolor); 376 | update(); 377 | } 378 | } 379 | 380 | void XYPlot::setMarkerEnabled(bool enabled) 381 | { 382 | if (m_marker != enabled) { 383 | m_marker = enabled; 384 | emit lineEnabledChanged(m_marker); 385 | update(); 386 | } 387 | } 388 | 389 | void XYPlot::setMarkerSegments(int n) 390 | { 391 | if (m_markersegments != n) { 392 | m_markersegments = n; 393 | emit markerSegmentsChanged(static_cast(m_markersegments)); 394 | update(); 395 | } 396 | } 397 | 398 | void XYPlot::setMarkerSize(double size) 399 | { 400 | if (m_markersize != size) { 401 | m_markersize = size; 402 | emit markerSizeChanged(m_markersize); 403 | update(); 404 | } 405 | } 406 | 407 | void XYPlot::setMarkerColor(const QColor &color) 408 | { 409 | if (m_markercolor != color) { 410 | m_markercolor = color; 411 | emit markerColorChanged(m_markercolor); 412 | update(); 413 | } 414 | } 415 | 416 | void XYPlot::setMarkerBorder(bool enabled) 417 | { 418 | if (m_markerborder != enabled) { 419 | m_markerborder = enabled; 420 | emit markerBorderChanged(m_markerborder); 421 | update(); 422 | } 423 | } 424 | 425 | void XYPlot::setLogY(bool enabled) 426 | { 427 | if (m_logy != enabled) { 428 | m_logy = enabled; 429 | emit logYChanged(m_logy); 430 | m_new_data = true; 431 | update(); 432 | } 433 | } 434 | 435 | 436 | class FillNode : public QSGGeometryNode 437 | { 438 | public: 439 | FillNode() { 440 | QSGGeometry* geometry; 441 | geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0); 442 | geometry->setDrawingMode(GL_TRIANGLE_STRIP); 443 | QSGMaterial* material; 444 | material = new XYFillMaterial; 445 | setGeometry(geometry); 446 | setFlag(QSGNode::OwnsGeometry); 447 | setMaterial(material); 448 | setFlag(QSGNode::OwnsMaterial); 449 | } 450 | ~FillNode() override = default; 451 | bool isSubtreeBlocked() const override { 452 | return m_blocked; 453 | } 454 | bool m_blocked = false; 455 | bool m_data_valid = false; 456 | }; 457 | 458 | 459 | class LineNode : public QSGGeometryNode 460 | { 461 | public: 462 | LineNode() { 463 | QSGGeometry* geometry; 464 | geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0); 465 | geometry->setDrawingMode(GL_LINE_STRIP); 466 | QSGMaterial* material; 467 | material = new XYLineMaterial; 468 | setGeometry(geometry); 469 | setFlag(QSGNode::OwnsGeometry); 470 | setMaterial(material); 471 | setFlag(QSGNode::OwnsMaterial); 472 | } 473 | ~LineNode() override = default; 474 | bool isSubtreeBlocked() const override { 475 | return m_blocked; 476 | } 477 | bool m_blocked = false; 478 | bool m_data_valid = false; 479 | }; 480 | 481 | 482 | class MarkerNode : public QSGGeometryNode 483 | { 484 | public: 485 | MarkerNode() { 486 | QSGGeometry* geometry; 487 | geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0); 488 | geometry->setDrawingMode(GL_POINTS); 489 | QSGMaterial* material; 490 | material = new XYMarkerMaterial; 491 | setGeometry(geometry); 492 | setFlag(QSGNode::OwnsGeometry); 493 | setMaterial(material); 494 | setFlag(QSGNode::OwnsMaterial); 495 | } 496 | ~MarkerNode() override = default; 497 | bool isSubtreeBlocked() const override { 498 | return m_blocked; 499 | } 500 | bool m_blocked = false; 501 | bool m_data_valid = false; 502 | }; 503 | 504 | 505 | static void paintPolygon(QImage& img, unsigned int segments, bool border) { 506 | int size = img.width(); 507 | QPainter p(&img); 508 | if (!border) { 509 | p.setPen(Qt::NoPen); 510 | } 511 | p.setBrush(QColor(Qt::white)); 512 | 513 | if (segments != 0) { 514 | auto points = std::vector(segments); 515 | double r = size * .5; 516 | double dphi = 2.*M_PI * (1./segments); 517 | for (unsigned int i = 0; i < segments; ++i) { 518 | double x = size*.5 - r * sin(i*dphi); 519 | double y = size*.5 - r * cos(i*dphi); 520 | points[i].setX(x); 521 | points[i].setY(y); 522 | } 523 | p.drawConvexPolygon(points.data(), static_cast(segments)); 524 | } else { 525 | p.drawEllipse(1, 1, size-2, size-2); 526 | } 527 | } 528 | 529 | QSGNode *XYPlot::updatePaintNode(QSGNode *n, QQuickItem::UpdatePaintNodeData *) 530 | { 531 | FillNode* n_fill; 532 | LineNode* n_line; 533 | MarkerNode* n_marker; 534 | QSGGeometry *fgeometry; 535 | XYFillMaterial *fmaterial; 536 | QSGGeometry *lgeometry; 537 | XYLineMaterial *lmaterial; 538 | QSGGeometry *mgeometry; 539 | XYMarkerMaterial *mmaterial; 540 | QSGNode::DirtyState dirty_state = QSGNode::DirtyMaterial; 541 | 542 | if (n == nullptr) { 543 | n = new QSGNode; 544 | } 545 | 546 | if (m_source == nullptr) { 547 | // remove child nodes if there is no data source 548 | if (n->childCount() != 0) { 549 | n_fill = static_cast(n->childAtIndex(0)); 550 | n_line = static_cast(n->childAtIndex(1)); 551 | n_marker = static_cast(n->childAtIndex(2)); 552 | n->removeAllChildNodes(); 553 | delete n_fill; 554 | delete n_line; 555 | delete n_marker; 556 | } 557 | // return empty node 558 | return n; 559 | } 560 | 561 | if (n->childCount() == 0) { 562 | // append child nodes for fill, line and markers 563 | n_fill = new FillNode; 564 | n_line = new LineNode; 565 | n_marker = new MarkerNode; 566 | n_fill->setFlag(QSGNode::OwnedByParent); 567 | n_line->setFlag(QSGNode::OwnedByParent); 568 | n_marker->setFlag(QSGNode::OwnedByParent); 569 | n->appendChildNode(n_fill); 570 | n->appendChildNode(n_line); 571 | n->appendChildNode(n_marker); 572 | } 573 | 574 | // ** graph node and data source can be considered valid from here on ** 575 | n_fill = static_cast(n->childAtIndex(0)); 576 | n_line = static_cast(n->childAtIndex(1)); 577 | n_marker = static_cast(n->childAtIndex(2)); 578 | fgeometry = n_fill->geometry(); 579 | lgeometry = n_line->geometry(); 580 | mgeometry = n_marker->geometry(); 581 | fmaterial = static_cast(n_fill->material()); 582 | lmaterial = static_cast(n_line->material()); 583 | mmaterial = static_cast(n_marker->material()); 584 | 585 | // check if fill, line or markers were switched on or off 586 | if (n_fill->m_blocked == m_fill || n_line->m_blocked == m_line || n_marker->m_blocked == m_marker) { 587 | n_fill->m_blocked = !m_fill; 588 | n_line->m_blocked = !m_line; 589 | n_marker->m_blocked = !m_marker; 590 | dirty_state |= QSGNode::DirtySubtreeBlocked; 591 | } 592 | 593 | int num_data_points = m_source->dataWidth() / 2; 594 | double xmin = m_view_rect.left(); 595 | double ymin = m_view_rect.top(); 596 | double xrange = m_view_rect.width(); 597 | double yrange = m_view_rect.height(); 598 | 599 | if (m_fill) { 600 | // update fill material parameters 601 | fmaterial->m_size.setWidth(width()); 602 | fmaterial->m_size.setHeight(height()); 603 | fmaterial->m_scale.setWidth(1. / xrange); 604 | fmaterial->m_scale.setHeight(1. / yrange); 605 | fmaterial->m_offset.setX(xmin); 606 | fmaterial->m_offset.setY(ymin); 607 | fmaterial->m_color = m_fillcolor; 608 | fmaterial->setFlag(QSGMaterial::Blending, m_fillcolor.alphaF() != 1.); 609 | 610 | // reallocate geometry if number of points changed 611 | if (fgeometry->vertexCount() != (2*num_data_points)) { 612 | fgeometry->allocate(2*num_data_points); 613 | m_new_data = true; 614 | } 615 | } 616 | 617 | if (m_line) { 618 | // update line material parameters 619 | lmaterial->m_size.setWidth(width()); 620 | lmaterial->m_size.setHeight(height()); 621 | lmaterial->m_scale.setWidth(1. / xrange); 622 | lmaterial->m_scale.setHeight(1. / yrange); 623 | lmaterial->m_offset.setX(xmin); 624 | lmaterial->m_offset.setY(ymin); 625 | lmaterial->m_color = m_linecolor; 626 | lmaterial->setFlag(QSGMaterial::Blending, m_linecolor.alphaF() != 1.); 627 | lgeometry->setLineWidth(static_cast(m_linewidth)); 628 | 629 | // reallocate geometry if number of points changed 630 | if (lgeometry->vertexCount() != num_data_points) { 631 | lgeometry->allocate(num_data_points); 632 | m_new_data = true; 633 | } 634 | } 635 | 636 | if (m_marker) { 637 | // update marker image 638 | if (mmaterial->m_markersize != m_markersize || 639 | mmaterial->m_markersegments != m_markersegments || 640 | mmaterial->m_markerborder != m_markerborder) { 641 | auto image_size = static_cast(std::ceil(m_markersize)); 642 | uint8_t* data = mmaterial->m_markerimage.allocateData2D(image_size, image_size, 4); 643 | QImage qimage(data, image_size, image_size, QImage::Format_ARGB32); 644 | qimage.fill(0x00ffffff); 645 | paintPolygon(qimage, static_cast(m_markersegments), m_markerborder); 646 | mmaterial->m_markerimage.commitData(); 647 | } 648 | 649 | // update marker material parameters 650 | mmaterial->m_size.setWidth(width()); 651 | mmaterial->m_size.setHeight(height()); 652 | mmaterial->m_scale.setWidth(1. / xrange); 653 | mmaterial->m_scale.setHeight(1. / yrange); 654 | mmaterial->m_offset.setX(xmin); 655 | mmaterial->m_offset.setY(ymin); 656 | mmaterial->m_markersegments = m_markersegments; 657 | mmaterial->m_markerborder = m_markerborder; 658 | mmaterial->m_markercolor = m_markercolor; 659 | mmaterial->m_markersize = m_markersize; 660 | mmaterial->setFlag(QSGMaterial::Blending); 661 | 662 | // reallocate geometry if number of points changed 663 | if (mgeometry->vertexCount() != num_data_points) { 664 | mgeometry->allocate(num_data_points); 665 | m_new_data = true; 666 | } 667 | } 668 | 669 | // update geometry if new data is available 670 | if (m_new_source || m_new_data) { 671 | n_fill->m_data_valid = false; 672 | n_line->m_data_valid = false; 673 | n_marker->m_data_valid = false; 674 | m_new_source = false; 675 | m_new_data = false; 676 | } 677 | 678 | auto* src = reinterpret_cast(m_source->data()); 679 | 680 | if (m_fill && !n_fill->m_data_valid) { 681 | auto* fdst = static_cast(fgeometry->vertexData()); 682 | for (int i = 0; i < num_data_points; ++i) { 683 | fdst[4*i+0] = static_cast(src[2*i+0]); 684 | fdst[4*i+1] = 0.; 685 | fdst[4*i+2] = static_cast(src[2*i+0]); 686 | fdst[4*i+3] = static_cast(src[2*i+1]); 687 | } 688 | dirty_state |= QSGNode::DirtyGeometry; 689 | n_fill->m_data_valid = true; 690 | } 691 | 692 | if (m_line && !n_line->m_data_valid) { 693 | auto* ldst = static_cast(lgeometry->vertexData()); 694 | if (m_logy) { 695 | for (int i = 0; i < num_data_points; ++i) { 696 | ldst[i*2 + 0] = static_cast(src[i*2 + 0]); 697 | ldst[i*2 + 1] = static_cast(std::log10(src[i*2 + 1])); 698 | } 699 | } else { 700 | for (int i = 0; i < num_data_points*2; ++i) { 701 | ldst[i] = static_cast(src[i]); 702 | } 703 | } 704 | dirty_state |= QSGNode::DirtyGeometry; 705 | n_line->m_data_valid = true; 706 | } 707 | 708 | if (m_marker && !n_marker->m_data_valid) { 709 | auto* mdst = static_cast(mgeometry->vertexData()); 710 | if (m_logy) { 711 | for (int i = 0; i < num_data_points; ++i) { 712 | mdst[i*2 + 0] = static_cast(src[i*2 + 0]); 713 | mdst[i*2 + 1] = static_cast(std::log10(src[i*2 + 1])); 714 | } 715 | } else { 716 | for (int i = 0; i < num_data_points*2; ++i) { 717 | mdst[i] = static_cast(src[i]); 718 | } 719 | } 720 | dirty_state |= QSGNode::DirtyGeometry; 721 | n_marker->m_data_valid = true; 722 | } 723 | 724 | n->markDirty(dirty_state); 725 | n_fill->markDirty(dirty_state); 726 | n_line->markDirty(dirty_state); 727 | n_marker->markDirty(dirty_state); 728 | return n; 729 | } 730 | -------------------------------------------------------------------------------- /src/qmlplotting/xyplot.h: -------------------------------------------------------------------------------- 1 | #ifndef XYPLOT_H 2 | #define XYPLOT_H 3 | 4 | #include "dataclient.h" 5 | 6 | class XYPlot : public DataClient 7 | { 8 | Q_OBJECT 9 | Q_PROPERTY(QRectF viewRect MEMBER m_view_rect WRITE setViewRect NOTIFY viewRectChanged) 10 | Q_PROPERTY(bool fillEnabled MEMBER m_fill WRITE setFillEnabled NOTIFY fillEnabledChanged) 11 | Q_PROPERTY(QColor fillColor MEMBER m_fillcolor WRITE setFillColor NOTIFY fillColorChanged) 12 | Q_PROPERTY(bool lineEnabled MEMBER m_line WRITE setLineEnabled NOTIFY lineEnabledChanged) 13 | Q_PROPERTY(double lineWidth MEMBER m_linewidth WRITE setLineWidth NOTIFY lineWidthChanged) 14 | Q_PROPERTY(QColor lineColor MEMBER m_linecolor WRITE setLineColor NOTIFY lineColorChanged) 15 | Q_PROPERTY(bool markerEnabled MEMBER m_marker WRITE setMarkerEnabled NOTIFY markerEnabledChanged) 16 | Q_PROPERTY(int markerSegments MEMBER m_markersegments WRITE setMarkerSegments NOTIFY markerSegmentsChanged) 17 | Q_PROPERTY(double markerSize MEMBER m_markersize WRITE setMarkerSize NOTIFY markerSizeChanged) 18 | Q_PROPERTY(QColor markerColor MEMBER m_markercolor WRITE setMarkerColor NOTIFY markerColorChanged) 19 | Q_PROPERTY(bool markerBorder MEMBER m_markerborder WRITE setMarkerBorder NOTIFY markerBorderChanged) 20 | Q_PROPERTY(bool logY MEMBER m_logy WRITE setLogY NOTIFY logYChanged) 21 | 22 | public: 23 | explicit XYPlot(QQuickItem *parent = nullptr); 24 | ~XYPlot() override = default; 25 | 26 | void setViewRect(const QRectF& viewrect); 27 | void setFillEnabled(bool enabled); 28 | void setFillColor(const QColor& color); 29 | void setLineEnabled(bool enabled); 30 | void setLineWidth(double width); 31 | void setLineColor(const QColor& color); 32 | void setMarkerEnabled(bool enabled); 33 | void setMarkerSegments(int n); 34 | void setMarkerSize(double size); 35 | void setMarkerColor(const QColor& color); 36 | void setMarkerBorder(bool enabled); 37 | void setLogY(bool enabled); 38 | 39 | signals: 40 | void viewRectChanged(const QRectF& viewrect); 41 | void fillEnabledChanged(bool); 42 | void fillColorChanged(const QColor&); 43 | void lineEnabledChanged(bool); 44 | void lineWidthChanged(double); 45 | void lineColorChanged(const QColor&); 46 | void markerEnabledChanged(bool); 47 | void markerSegmentsChanged(int); 48 | void markerSizeChanged(double); 49 | void markerColorChanged(const QColor&); 50 | void markerBorderChanged(bool); 51 | void logYChanged(bool); 52 | 53 | protected: 54 | QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* updatePaintNodeData) override; 55 | 56 | private: 57 | QRectF m_view_rect = {0, 0, 1, 1}; 58 | bool m_fill = false; 59 | QColor m_fillcolor = {0, 0, 0}; 60 | bool m_line = true; 61 | double m_linewidth = 1.; 62 | QColor m_linecolor = {0, 0, 0}; 63 | bool m_marker = true; 64 | double m_markersegments = 0; 65 | double m_markersize = 5.; 66 | QColor m_markercolor = {0, 0, 0}; 67 | bool m_markerborder = false; 68 | bool m_logy = false; 69 | }; 70 | 71 | #endif // XYPLOT_H 72 | -------------------------------------------------------------------------------- /src/ressources/Axes.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.7 2 | import QtQuick.Layouts 1.1 3 | import QmlPlotting 2.0 as QmlPlotting 4 | 5 | Rectangle { 6 | id: root 7 | width: 400 8 | height: 300 9 | color: "transparent" 10 | 11 | property string xLabel: "" 12 | property string yLabel: "" 13 | property int tickXSpacing: 30 14 | property int tickYSpacing: 30 15 | property bool tickXLabels: true 16 | property bool tickYLabels: true 17 | property bool tickXGrid: true 18 | property bool tickYGrid: true 19 | property bool tickXMarker: true 20 | property bool tickYMarker: true 21 | property alias font: textMetric.font 22 | property color gridColor: "lightgray" 23 | property color textColor: "black" 24 | property int axesBorderWidth: 1 25 | property color axesBorderColor: "black" 26 | property color axesBackgroundColor: "transparent" 27 | 28 | property Item plotGroup: QmlPlotting.PlotGroup { 29 | Component.onCompleted: { 30 | plotGroup.parent = plotGroupItem; 31 | plotGroup.anchors.fill = plotGroupItem; 32 | } 33 | } 34 | 35 | onPlotGroupChanged: { 36 | // Reparent and anchor assigned plot group 37 | plotGroup.parent = plotGroupItem; 38 | plotGroup.anchors.fill = plotGroupItem; 39 | } 40 | 41 | TextMetrics { 42 | id: textMetric 43 | text: "-1000.00" 44 | } 45 | 46 | QtObject { 47 | id: xTicks 48 | property var model: calculateTicks(root.plotGroup.viewRect, plotGroupItem.width, root.tickXSpacing, textMetric.width) 49 | function calculateTicks(viewRect, width, spacing, text_width) { 50 | var x0 = viewRect.x; 51 | var xrange = viewRect.width; 52 | 53 | var max_ticks = Math.max(Math.floor(width / (text_width + spacing)) + 1, 2); 54 | var tickdiffprec = QmlPlotting.Utils.niceNumPrec(xrange / (max_ticks - 1), false); 55 | var tick_diff = tickdiffprec[0]; 56 | var tick_prec = tickdiffprec[1]; 57 | var tick_min = Math.ceil(x0 / tick_diff) * tick_diff; 58 | var n_ticks = Math.ceil((x0 + xrange - tick_min) / tick_diff); 59 | var tick_max = tick_min + n_ticks * tick_diff; 60 | 61 | var newTicks = []; 62 | for (var i=0; i < n_ticks; ++i) { 63 | var x = tick_min + tick_diff * i; 64 | var pos_x = (x - x0) * (width / xrange); 65 | if (pos_x > 0 && pos_x < width) { 66 | var text = x.toFixed(tick_prec); 67 | newTicks.push({pos: pos_x, text: text}); 68 | } 69 | } 70 | return newTicks; 71 | } 72 | } 73 | 74 | QtObject { 75 | id: yTicks 76 | property var model: calculateTicks(root.plotGroup.viewRect, plotGroupItem.height, root.tickYSpacing, textMetric.height, root.plotGroup.logY) 77 | function calculateTicks(viewRect, height, spacing, text_height, logY) { 78 | var y0 = viewRect.y; 79 | var yrange = viewRect.height; 80 | 81 | var max_ticks = Math.max(Math.floor(height / (text_height + spacing)) + 1, 2); 82 | var tickdiffprec = QmlPlotting.Utils.niceNumPrec(yrange / (max_ticks - 1), logY); 83 | var tick_diff = tickdiffprec[0]; 84 | var tick_prec = tickdiffprec[1]; 85 | var tick_min = Math.floor(y0 / tick_diff) * tick_diff; 86 | var n_ticks = Math.ceil((y0 + yrange - tick_min) / tick_diff); 87 | var tick_max = tick_min + n_ticks * tick_diff; 88 | 89 | var newTicks = []; 90 | for (var i=0; i < n_ticks; ++i) { 91 | var y = tick_min + tick_diff * i; 92 | var pos_y = height - ((y - y0) * (height / yrange)); 93 | if (pos_y > 0 && pos_y < height) { 94 | var text = logY ? Math.pow(10, y).toExponential() : y.toFixed(tick_prec); 95 | newTicks.push({pos: pos_y, text: text}); 96 | } 97 | } 98 | return newTicks; 99 | } 100 | } 101 | 102 | GridLayout { 103 | anchors.fill: parent 104 | anchors.margins: 10 105 | columns: 3 106 | rowSpacing: 5 107 | columnSpacing: 5 108 | 109 | // Y axis label 110 | Item { 111 | width: textMetric.height 112 | visible: yLabel 113 | 114 | Layout.row: 0 115 | Layout.column: 0 116 | Layout.fillHeight: true 117 | 118 | Text { 119 | anchors.centerIn: parent 120 | text: yLabel 121 | rotation: -90 122 | font: textMetric.font 123 | color: root.textColor 124 | } 125 | } 126 | 127 | // Y tick labels 128 | Item { 129 | width: textMetric.width 130 | visible: tickYLabels 131 | 132 | Layout.row: 0 133 | Layout.column: 1 134 | Layout.fillHeight: true 135 | 136 | Repeater { 137 | model: yTicks.model.length 138 | delegate: Text { 139 | y: yTicks.model[index].pos - .5*height 140 | text: yTicks.model[index].text 141 | font: textMetric.font 142 | color: root.textColor 143 | width: textMetric.width 144 | height: textMetric.height 145 | horizontalAlignment: Text.AlignRight 146 | verticalAlignment: Text.AlignVCenter 147 | } 148 | } 149 | } 150 | 151 | Item { 152 | id: plotGroupItem 153 | Layout.row: 0 154 | Layout.column: 2 155 | Layout.fillHeight: true 156 | Layout.fillWidth: true 157 | 158 | // X grid lines 159 | Repeater { 160 | model: xTicks.model.length 161 | delegate: Rectangle { 162 | visible: tickXGrid 163 | color: root.gridColor 164 | x: xTicks.model[index].pos 165 | y: plotGroupItem.height - height 166 | z: -1 167 | width: 1 168 | height: plotGroupItem.height 169 | } 170 | } 171 | // X tick markers 172 | Repeater { 173 | model: xTicks.model.length 174 | delegate: Rectangle { 175 | visible: tickXMarker 176 | color: root.textColor 177 | x: xTicks.model[index].pos 178 | y: plotGroupItem.height - height 179 | z: 1 180 | width: 1 181 | height: 10 182 | } 183 | } 184 | // X grid lines 185 | Repeater { 186 | model: yTicks.model.length 187 | delegate: Rectangle { 188 | visible: tickYGrid 189 | color: root.gridColor 190 | x: 0 191 | y: yTicks.model[index].pos 192 | z: -1 193 | height: 1 194 | width: plotGroupItem.width 195 | } 196 | } 197 | // Y tick markers 198 | Repeater { 199 | model: yTicks.model.length 200 | delegate: Rectangle { 201 | visible: tickYMarker 202 | color: root.textColor 203 | x: 0 204 | y: yTicks.model[index].pos 205 | z: 1 206 | height: 1 207 | width: 10 208 | } 209 | } 210 | // Plot background 211 | Rectangle { 212 | anchors.fill: parent 213 | color: axesBackgroundColor 214 | z: -2 215 | } 216 | // Plot frame 217 | Rectangle { 218 | anchors.fill: parent 219 | color: "transparent" 220 | border.color: axesBorderColor 221 | border.width: axesBorderWidth 222 | z: 1 223 | } 224 | } 225 | 226 | // X tick labels 227 | Item { 228 | height: textMetric.height 229 | visible: tickXLabels 230 | 231 | Layout.row: 1 232 | Layout.column: 2 233 | Layout.fillWidth: true 234 | 235 | Repeater { 236 | model: xTicks.model.length 237 | delegate: Text { 238 | x: xTicks.model[index].pos - textMetric.width*.5 239 | text: xTicks.model[index].text 240 | font: textMetric.font 241 | color: root.textColor 242 | width: textMetric.width 243 | height: textMetric.height 244 | horizontalAlignment: Text.AlignHCenter 245 | verticalAlignment: Text.AlignTop 246 | } 247 | } 248 | } 249 | 250 | // X axis label 251 | Item { 252 | height: textMetric.height 253 | visible: xLabel 254 | 255 | Layout.row: 2 256 | Layout.column: 2 257 | Layout.fillWidth: true 258 | 259 | Text { 260 | anchors.centerIn: parent 261 | text: xLabel 262 | font: textMetric.font 263 | color: root.textColor 264 | } 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/ressources/Container.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.7 2 | 3 | Item { 4 | id: root 5 | property rect viewRect 6 | property rect itemRect: Qt.rect(0, 0, 1, 1) 7 | 8 | default property alias contentItems: contentItem.children 9 | 10 | Item { 11 | id: contentItem 12 | readonly property real _xFactor: root.width / root.viewRect.width 13 | readonly property real _yFactor: root.height / root.viewRect.height 14 | width: root.itemRect.width * _xFactor 15 | height: root.itemRect.height * _yFactor 16 | x: (itemRect.x - viewRect.x) * _xFactor 17 | y: root.height - height - (root.itemRect.y - root.viewRect.y) * _yFactor 18 | 19 | onChildrenChanged: Qt.callLater(function () { 20 | // Anchor children to container content item 21 | // TODO: Accessing children within onChildrenChanged crashes in Windows-Release. Why? 22 | for (var i=0; i < children.length; ++i) { 23 | children[i].anchors.fill = contentItem; 24 | } 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ressources/MovableContainer.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.7 2 | import QmlPlotting 2.0 as QmlPlotting 3 | 4 | QmlPlotting.Container { 5 | id: root 6 | property bool resizable: true 7 | property bool movable: true 8 | property bool aspectFixed: false 9 | property rect maxItemRect: Qt.rect(-Infinity, -Infinity, Infinity, Infinity) 10 | 11 | Item { 12 | id: frame 13 | visible: root.movable || root.resizable 14 | 15 | MouseArea { 16 | enabled: root.movable 17 | anchors.fill: parent 18 | cursorShape: Qt.OpenHandCursor 19 | 20 | onPressed: { 21 | frame.pressedCoords = mapToGlobal(mouse.x, mouse.y); 22 | frame.pressedItemRect = root.itemRect; 23 | cursorShape = Qt.ClosedHandCursor; 24 | } 25 | onReleased: { cursorShape = Qt.OpenHandCursor; } 26 | onPositionChanged: { 27 | // Map new position to axes coordinates 28 | const pressedRect = frame.pressedItemRect; 29 | const pNow = mapToGlobal(mouse.x, mouse.y); 30 | let itemX = pressedRect.x + (pNow.x - frame.pressedCoords.x) * root.viewRect.width / root.width; 31 | let itemY = pressedRect.y + (frame.pressedCoords.y - pNow.y) * root.viewRect.height / root.height; 32 | // Restrict new position movement to max rect 33 | const maxRect = root.maxItemRect; 34 | if (maxRect.width !== Infinity) { 35 | itemX = Math.min(maxRect.x + maxRect.width - pressedRect.width, Math.max(maxRect.x, itemX)); 36 | } 37 | if (maxRect.height !== Infinity) { 38 | itemY = Math.min(maxRect.y + maxRect.height - pressedRect.height, Math.max(maxRect.y, itemY)); 39 | } 40 | // Apply 41 | root.itemRect = Qt.rect(itemX, itemY, pressedRect.width, pressedRect.height); 42 | } 43 | } 44 | 45 | Rectangle { 46 | anchors.fill: parent 47 | anchors.margins: -2 48 | color: "transparent" 49 | border.color: "gray" 50 | border.width: 2 51 | } 52 | 53 | property point pressedCoords 54 | property rect pressedItemRect 55 | 56 | // Top left handle 57 | Rectangle { 58 | visible: root.resizable 59 | enabled: root.resizable 60 | x: -width * .5 61 | y: -height * .5 62 | width: 14 63 | height: 14 64 | radius: 7 65 | color: "lightgray" 66 | border.color: "black" 67 | border.width: 1 68 | MouseArea { 69 | anchors.fill: parent 70 | cursorShape: Qt.SizeFDiagCursor 71 | onPressed: { 72 | frame.pressedCoords = mapToGlobal(mouse.x, mouse.y); 73 | frame.pressedItemRect = root.itemRect; 74 | } 75 | onPositionChanged: { 76 | // Calculate delta from initial, pressed state 77 | const pressedRect = frame.pressedItemRect; 78 | const pNow = mapToGlobal(mouse.x, mouse.y); 79 | const delta = Qt.point((pNow.x - frame.pressedCoords.x) * root.viewRect.width / root.width, 80 | (frame.pressedCoords.y - pNow.y) * root.viewRect.height / root.height); 81 | // Restrict size reduction 82 | const maxReduction = 0.9; 83 | delta.x = Math.min(delta.x, pressedRect.width * maxReduction); 84 | delta.y = Math.max(delta.y, -pressedRect.height * maxReduction); 85 | // Restrict movement by aspect (TODO: implement non 1:1) 86 | if (root.aspectFixed) { 87 | const deltaMean = .5 * (delta.x - delta.y); 88 | delta.x = deltaMean; delta.y = -deltaMean; 89 | } 90 | // Clamp deltas to max rect 91 | const maxRect = root.maxItemRect; 92 | delta.x = Math.max(maxRect.x, delta.x + pressedRect.x) - pressedRect.x; 93 | if (root.aspectFixed) { delta.y = -delta.x; } 94 | if (maxRect.height !== Infinity) { 95 | delta.y = Math.min(maxRect.y + maxRect.height, pressedRect.y + pressedRect.height + delta.y) - (pressedRect.y + pressedRect.height); 96 | } 97 | if (root.aspectFixed) { delta.x = -delta.y; } 98 | // Apply to rect position / size 99 | root.itemRect = Qt.rect( 100 | pressedRect.x + delta.x, pressedRect.y, 101 | pressedRect.width - delta.x, pressedRect.height + delta.y); 102 | } 103 | } 104 | } 105 | // Bottom right handle (TODO: Use common resize handle definition) 106 | Rectangle { 107 | visible: root.resizable 108 | enabled: root.resizable 109 | x: parent.width - width * .5 110 | y: parent.height - height * .5 111 | width: 14 112 | height: 14 113 | radius: 7 114 | color: "lightgray" 115 | border.color: "black" 116 | border.width: 1 117 | MouseArea { 118 | anchors.fill: parent 119 | cursorShape: Qt.SizeFDiagCursor 120 | onPressed: { 121 | frame.pressedCoords = mapToGlobal(mouse.x, mouse.y); 122 | frame.pressedItemRect = root.itemRect; 123 | } 124 | onPositionChanged: { 125 | // Calculate delta from initial, pressed state 126 | const pressedRect = frame.pressedItemRect; 127 | const pNow = mapToGlobal(mouse.x, mouse.y); 128 | const delta = Qt.point((pNow.x - frame.pressedCoords.x) * root.viewRect.width / root.width, 129 | (frame.pressedCoords.y - pNow.y) * root.viewRect.height / root.height); 130 | // Restrict size reduction 131 | const maxReduction = 0.9; 132 | delta.x = Math.max(delta.x, -pressedRect.width * maxReduction); 133 | delta.y = Math.min(delta.y, pressedRect.height * maxReduction); 134 | // Restrict movement by aspect (TODO: implement non 1:1) 135 | if (root.aspectFixed) { 136 | const deltaMean = .5 * (delta.x - delta.y); 137 | delta.x = deltaMean; delta.y = -deltaMean; 138 | } 139 | // Clamp deltas to max rect 140 | const maxRect = root.maxItemRect; 141 | delta.y = Math.max(maxRect.y, delta.y + pressedRect.y) - pressedRect.y; 142 | if (root.aspectFixed) { delta.x = -delta.y; } 143 | if (maxRect.width !== Infinity) { 144 | delta.x = Math.min(maxRect.x + maxRect.width, pressedRect.x + pressedRect.width + delta.x) - (pressedRect.x + pressedRect.width); 145 | } 146 | if (root.aspectFixed) { delta.y = -delta.x; } 147 | // Apply to rect position / size 148 | root.itemRect = Qt.rect( 149 | pressedRect.x, pressedRect.y + delta.y, 150 | pressedRect.width + delta.x, pressedRect.height - delta.y); 151 | } 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/ressources/Scale.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.7 2 | import QmlPlotting 2.0 as QmlPlotting 3 | 4 | Rectangle { 5 | id: root 6 | 7 | // Style settings 8 | color: "#ccffffff" 9 | border.color: "black" 10 | border.width: 1. 11 | property real padding: 2 12 | property alias font: label.font 13 | 14 | // Scale control 15 | property Item plotItem: parent 16 | property string displayText: scaleText 17 | property real scaleMinWidth: .25 * parent.width 18 | property real scaleFactor: 1. 19 | 20 | readonly property real _fromSceneFactor: scaleFactor * plotItem.viewRect.width / plotItem.width; 21 | readonly property var _niceScaleTuple: QmlPlotting.Utils.niceNumPrec(_fromSceneFactor * scaleMinWidth) 22 | readonly property real scaleValue: _niceScaleTuple[0] 23 | readonly property real scalePrecision: _niceScaleTuple[1] 24 | readonly property string scaleText: scaleValue.toFixed(scalePrecision) 25 | width: scaleValue / _fromSceneFactor; 26 | implicitHeight: label.implicitHeight 27 | 28 | Text { 29 | anchors.centerIn: parent 30 | id: label 31 | text: root.displayText 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ressources/Utils.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | 3 | /** 4 | * @brief niceNumPrec Return 'nice' number and precision suggestion from input. 5 | * @param number Input number. 6 | * @param logMode Return result suitable for log scale. 7 | * @return Nice number and suggested precision. 8 | */ 9 | function niceNumPrec(number, logMode) { 10 | var exponent = Math.floor(Math.log(number) / Math.LN10); 11 | var fraction = number / Math.pow(10, exponent); 12 | var nicePrecision = -exponent; 13 | var nicefrac; 14 | if (fraction <= 1) nicefrac = 1; 15 | else if (fraction <= 2) nicefrac = 2; 16 | else if (fraction <= 2.5) {nicefrac = 2.5; nicePrecision += 1} 17 | else if (fraction <= 5) nicefrac = 5; 18 | else {nicefrac = 10; nicePrecision -= 1} 19 | var niceNumber = nicefrac * Math.pow(10, exponent); 20 | if (logMode) 21 | return [Math.ceil(Math.max(1, niceNumber)), Math.max(nicePrecision, 0)] 22 | else 23 | return [niceNumber, Math.max(nicePrecision, 0)] 24 | } 25 | -------------------------------------------------------------------------------- /src/ressources/ZoomPanTool.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.7 2 | import QmlPlotting 2.0 as QmlPlotting 3 | 4 | Item { 5 | id: root 6 | 7 | property Item plotGroup: parent 8 | 9 | readonly property bool pressed: mouseArea.pressed || mouseArea.wheelActive || pinchArea.pressed 10 | 11 | property rect maxViewRect: Qt.rect(-Infinity, -Infinity, Infinity, Infinity) 12 | property size minimumSize: Qt.size(0, 0) 13 | property size maximumSize: Qt.size(maxViewRect.width, maxViewRect.height) 14 | 15 | function scaleSizeVarAspect(oldSize, scaleX, scaleY) { 16 | var width = Math.max(Math.min(oldSize.width*scaleX, maximumSize.width), minimumSize.width); 17 | var height = Math.max(Math.min(oldSize.height*scaleY, maximumSize.height), minimumSize.height); 18 | return Qt.size(width, height); 19 | } 20 | 21 | function scaleSizeFixAspect(oldSize, scale) { 22 | var scaleX = Math.max(Math.min(oldSize.width*scale, maximumSize.width), minimumSize.width) / oldSize.width; 23 | var scaleY = Math.max(Math.min(oldSize.height*scale, maximumSize.height), minimumSize.height) / oldSize.height; 24 | // TODO: Allow chosing between different clip/expand policies? 25 | var scaleMax = Math.max(scaleX, scaleY); 26 | return Qt.size(oldSize.width * scaleMax, oldSize.height * scaleMax); 27 | } 28 | 29 | function moveToMaxView(rect) { 30 | var x, y; 31 | // Adjust view to valid position or center view if size exceeds limit 32 | if (rect.width > maxViewRect.width) { 33 | x = maxViewRect.x + (maxViewRect.width - rect.width) * .5; 34 | } else { 35 | var xMax = (maxViewRect.width === Infinity) ? Infinity : maxViewRect.x + maxViewRect.width - rect.width; 36 | x = Math.min(Math.max(rect.x, maxViewRect.x), xMax); 37 | } 38 | if (rect.height > maxViewRect.height) { 39 | y = maxViewRect.y + (maxViewRect.height - rect.height) * .5; 40 | } else { 41 | var yMax = (maxViewRect.height === Infinity) ? Infinity : maxViewRect.y + maxViewRect.height - rect.height; 42 | y = Math.min(Math.max(rect.y, maxViewRect.y), yMax); 43 | } 44 | return Qt.rect(x, y, rect.width, rect.height); 45 | } 46 | 47 | // Mouse/Pinch area for zooming and panning viewRect 48 | PinchArea { 49 | id: pinchArea 50 | enabled: root.enabled 51 | property bool pressed: false 52 | anchors.fill: parent 53 | onPinchStarted: { pressed = true; } 54 | onPinchFinished: { pressed = false; } 55 | onPinchUpdated: { 56 | // TODO: Check new coordinate transform on touch device 57 | // TODO: Apply constraints 58 | var viewRect = root.plotGroup.viewRect; 59 | var newViewRect = Qt.rect(viewRect.x, viewRect.y, viewRect.width, viewRect.height); 60 | // Calculate pinch center move in plot space 61 | var delta = Qt.point((pinch.center.x - pinch.previousCenter.x) * viewRect.width / root.width, 62 | (pinch.previousCenter.y - pinch.center.y) * viewRect.height / root.height); 63 | var scale = 1. / (1. + pinch.scale - pinch.previousScale); 64 | 65 | // Apply pan 66 | newViewRect.x -= delta.x; 67 | newViewRect.y -= delta.y; 68 | // Apply zoom 69 | newViewRect.x = newViewRect.x + pinch.center.x / root.width * newViewRect.width * (1. - scale); 70 | newViewRect.y = newViewRect.y + (root.height - pinch.center.y) / root.height * newViewRect.height * (1. - scale); 71 | newViewRect.width *= scale; 72 | newViewRect.height *= scale; 73 | root.plotGroup.viewRect = newViewRect; 74 | } 75 | 76 | MouseArea { 77 | id: mouseArea 78 | property bool wheelActive: false 79 | property point mouseOld: Qt.point(0, 0) 80 | property point pressedP: Qt.point(0, 0) 81 | property bool zoomMode: false 82 | acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MidButton 83 | enabled: root.enabled 84 | anchors.fill: parent 85 | 86 | onWheel: { 87 | var oldPos = Qt.point(root.plotGroup.viewRect.x, root.plotGroup.viewRect.y); 88 | var oldSize = Qt.size(root.plotGroup.viewRect.width, root.plotGroup.viewRect.height); 89 | var scale = 1. - wheel.angleDelta.y * (0.25 / 120.); 90 | const autoAspect = root.plotGroup.viewMode === QmlPlotting.PlotGroup.AutoAspect; 91 | var newSize = autoAspect ? scaleSizeVarAspect(oldSize, scale, scale) : scaleSizeFixAspect(oldSize, scale); 92 | var x = oldPos.x + wheel.x/root.width * (oldSize.width - newSize.width); 93 | var y = oldPos.y + (1. - wheel.y/root.height) * (oldSize.height - newSize.height); 94 | var newRect = Qt.rect(x, y, newSize.width, newSize.height); 95 | wheelActive = true; 96 | root.plotGroup.viewRect = moveToMaxView(newRect); 97 | wheelActive = false; 98 | } 99 | onPressed: { 100 | // Store coordinates and set zoom/pan mode on mouse-down 101 | pressedP = Qt.point(mouse.x, mouse.y); 102 | mouseOld = Qt.point(mouse.x, mouse.y); 103 | zoomMode = pressedButtons & Qt.RightButton; 104 | } 105 | onPositionChanged: { 106 | var oldPos = Qt.point(root.plotGroup.viewRect.x, root.plotGroup.viewRect.y); 107 | var oldSize = Qt.size(root.plotGroup.viewRect.width, root.plotGroup.viewRect.height); 108 | // Calculate mouse delta in plot space 109 | var delta = Qt.point((mouse.x - mouseOld.x) * oldSize.width / root.width, 110 | (mouseOld.y - mouse.y) * oldSize.height / root.height); 111 | mouseOld = Qt.point(mouse.x, mouse.y); 112 | 113 | var newRect, x, y; 114 | if (zoomMode) { 115 | // Zoom mode, scale view at mouse down coordinates 116 | var newSize; 117 | const autoAspect = root.plotGroup.viewMode === QmlPlotting.PlotGroup.AutoAspect; 118 | if (!autoAspect) { 119 | var relDelta = Math.abs(delta.x) > Math.abs(delta.y) ? delta.x/oldSize.width : delta.y/oldSize.height; 120 | var scale = 1. - 2. * relDelta; 121 | newSize = scaleSizeFixAspect(oldSize, scale); 122 | } else { 123 | var scaleX = 1. - 2. * delta.x / oldSize.width; 124 | var scaleY = 1. - 2. * delta.y / oldSize.height; 125 | newSize = scaleSizeVarAspect(oldSize, scaleX, scaleY); 126 | } 127 | var pressedCoords = Qt.point(pressedP.x * oldSize.width / root.width, 128 | (root.height - pressedP.y) * oldSize.height / root.height); 129 | x = oldPos.x + pressedCoords.x * (1. - newSize.width/oldSize.width); 130 | y = oldPos.y + pressedCoords.y * (1. - newSize.height/oldSize.height); 131 | newRect = Qt.rect(x, y, newSize.width, newSize.height); 132 | } else { 133 | // Pan mode, move view according to mouse delta 134 | x = oldPos.x - delta.x; 135 | y = oldPos.y - delta.y; 136 | newRect = Qt.rect(x, y, oldSize.width, oldSize.height); 137 | } 138 | root.plotGroup.viewRect = moveToMaxView(newRect); 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/ressources/qmldir: -------------------------------------------------------------------------------- 1 | module QmlPlotting 2 | plugin qmlplottingplugin 3 | classname QmlPlottingPlugin 4 | 5 | Utils 2.0 Utils.js 6 | Scale 2.0 Scale.qml 7 | Axes 2.0 Axes.qml 8 | Container 2.0 Container.qml 9 | MovableContainer 2.0 MovableContainer.qml 10 | ZoomPanTool 2.0 ZoomPanTool.qml 11 | -------------------------------------------------------------------------------- /tests/auto/tst_basic.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static void initialize() 4 | { 5 | qputenv("QML2_IMPORT_PATH", QMLPLOTTING_IMPORT_DIR); 6 | } 7 | 8 | Q_COREAPP_STARTUP_FUNCTION(initialize) 9 | QUICK_TEST_MAIN(basic) 10 | -------------------------------------------------------------------------------- /tests/auto/tst_basic.qbs: -------------------------------------------------------------------------------- 1 | import qbs 2 | import qbs.FileInfo 3 | 4 | CppApplication { 5 | builtByDefault: false 6 | Depends { name: "Qt"; submodules: [ "quick", "testlib", "qmltest" ] } 7 | Depends { name: "qmlplotting"} 8 | cpp.cxxLanguageVersion: "c++17" 9 | 10 | property path qmlplottingImportDir: FileInfo.joinPaths(project.buildDirectory, "install-root") 11 | property pathList qmlImportPaths: [ qmlplottingImportDir ] 12 | cpp.defines: [ 13 | "QMLPLOTTING_IMPORT_DIR=\"" + FileInfo.fromWindowsSeparators(qmlplottingImportDir) + "\"", 14 | "QUICK_TEST_SOURCE_DIR=\"" + FileInfo.fromWindowsSeparators(product.sourceDirectory) + "\"", 15 | ] 16 | 17 | files: [ 18 | "tst_basic.cpp", 19 | "tst_basic.qml", 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tests/auto/tst_basic.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.7 2 | import QtTest 1.0 3 | import QmlPlotting 2.0 as QmlPlotting 4 | 5 | QmlPlotting.PlotGroup { 6 | id: plotGroup 7 | width: 512 8 | height: 512 9 | 10 | // Add items under test to plot group 11 | plotItems: [ 12 | QmlPlotting.ColormappedImage { 13 | id: colormappedImage 14 | dataSource: QmlPlotting.DataSource {} 15 | }, 16 | QmlPlotting.XYPlot { 17 | id: xyPlot 18 | dataSource: QmlPlotting.DataSource {} 19 | } 20 | ] 21 | 22 | TestCase { 23 | name: "XYPlot" 24 | function test_setTestData() { 25 | xyPlot.dataSource.setTestData1D(); 26 | } 27 | } 28 | 29 | TestCase { 30 | name: "ColormappedImage" 31 | function test_setTestData() { 32 | colormappedImage.dataSource.setTestData2D(); 33 | } 34 | } 35 | 36 | TestCase { 37 | name: "PlotGroup" 38 | function test_viewRectBinding() { 39 | plotGroup.viewRect = Qt.rect(-1, -1, 2, 2); 40 | compare(plotGroup.viewRect, xyPlot.viewRect); 41 | compare(plotGroup.viewRect, colormappedImage.viewRect); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/tests.qbs: -------------------------------------------------------------------------------- 1 | import qbs 2 | 3 | Project { 4 | references: [ 5 | "auto/tst_basic.qbs", 6 | ] 7 | } 8 | 9 | --------------------------------------------------------------------------------