Change in plasma-framework[master]: Add a plotter component
Marco Martin (Code Review)
noreply at kde.org
Tue Dec 16 13:13:49 UTC 2014
Marco Martin has uploaded a new change for review.
https://gerrit.vesnicky.cesnet.cz/r/244
Change subject: Add a plotter component
......................................................................
Add a plotter component
a Plotter can draw a graph of values arriving from an arbitrary number of data sources
to show their evoluton in time.
an example can be a plot of the network transfer speed or CPU temperature over time.
Multiple plots can be fitted in the same graph, either stacked or intersected.
Change-Id: I4a29a25412b375dfbd7f0db618040bae19c9b39c
---
M CMakeLists.txt
A Findepoxy.cmake
M src/declarativeimports/plasmaextracomponents/CMakeLists.txt
M src/declarativeimports/plasmaextracomponents/plasmaextracomponentsplugin.cpp
A src/declarativeimports/plasmaextracomponents/plotter.cpp
A src/declarativeimports/plasmaextracomponents/plotter.h
A tests/plotter.qml
7 files changed, 993 insertions(+), 3 deletions(-)
git pull ssh://gerrit.vesnicky.cesnet.cz:29418/plasma-framework refs/changes/44/244/1
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b19ab53..2a971a0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,7 +4,7 @@
# ECM setup
find_package(ECM 1.4.0 REQUIRED NO_MODULE)
-set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR})
+set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR})
include(FeatureSummary)
include(GenerateExportHeader)
@@ -109,6 +109,13 @@
set(HAVE_GLX 0)
endif()
+find_package(epoxy)
+set_package_properties(epoxy PROPERTIES DESCRIPTION "libepoxy"
+ URL "http://github.com/anholt/libepoxy"
+ TYPE REQUIRED
+ PURPOSE "OpenGL dispatch library"
+ )
+
#########################################################################
add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0)
@@ -164,6 +171,7 @@
# make plasma_version.h available
include_directories(${CMAKE_CURRENT_BINARY_DIR})
+include_directories(${epoxy_INCLUDE_DIR})
################# list the subdirectories #################
if (KF5DocTools_FOUND)
diff --git a/Findepoxy.cmake b/Findepoxy.cmake
new file mode 100644
index 0000000..c5500a0
--- /dev/null
+++ b/Findepoxy.cmake
@@ -0,0 +1,27 @@
+# - Try to find libepoxy
+# Once done this will define
+#
+# epoxy_FOUND - System has libepoxy
+# epoxy_LIBRARY - The libepoxy library
+# epoxy_INCLUDE_DIR - The libepoxy include dir
+# epoxy_DEFINITIONS - Compiler switches required for using libepoxy
+
+# Copyright (c) 2014 Fredrik Höglund <fredrik at kde.org>
+#
+# Redistribution and use is allowed according to the terms of the BSD license.
+# For details see the accompanying COPYING-CMAKE-SCRIPTS file.
+
+if (NOT WIN32)
+ find_package(PkgConfig)
+ pkg_check_modules(PKG_epoxy QUIET epoxy)
+
+ set(epoxy_DEFINITIONS ${PKG_epoxy_CFLAGS})
+
+ find_path(epoxy_INCLUDE_DIR NAMES epoxy/gl.h HINTS ${PKG_epoxy_INCLUDEDIR} ${PKG_epoxy_INCLUDE_DIRS})
+ find_library(epoxy_LIBRARY NAMES epoxy HINTS ${PKG_epoxy_LIBDIR} ${PKG_epoxy_LIBRARY_DIRS})
+
+ include(FindPackageHandleStandardArgs)
+ find_package_handle_standard_args(epoxy DEFAULT_MSG epoxy_LIBRARY epoxy_INCLUDE_DIR)
+
+ mark_as_advanced(epoxy_INCLUDE_DIR epoxy_LIBRARY)
+endif()
diff --git a/src/declarativeimports/plasmaextracomponents/CMakeLists.txt b/src/declarativeimports/plasmaextracomponents/CMakeLists.txt
index 8aec3c5..b13f345 100644
--- a/src/declarativeimports/plasmaextracomponents/CMakeLists.txt
+++ b/src/declarativeimports/plasmaextracomponents/CMakeLists.txt
@@ -10,6 +10,7 @@
#resourceinstance.cpp
plasmaextracomponentsplugin.cpp
fallbackcomponent.cpp
+ plotter.cpp
)
add_library(plasmaextracomponentsplugin SHARED ${plasmaextracomponents_SRCS})
@@ -19,7 +20,8 @@
Qt5::Qml
${KACTIVITIES_LIBRARY}
KF5::Service
- KF5::Plasma)
+ KF5::Plasma
+ ${epoxy_LIBRARY})
install(TARGETS plasmaextracomponentsplugin DESTINATION ${QML_INSTALL_DIR}/org/kde/plasma/extras)
diff --git a/src/declarativeimports/plasmaextracomponents/plasmaextracomponentsplugin.cpp b/src/declarativeimports/plasmaextracomponents/plasmaextracomponentsplugin.cpp
index 448e13a..2506902 100644
--- a/src/declarativeimports/plasmaextracomponents/plasmaextracomponentsplugin.cpp
+++ b/src/declarativeimports/plasmaextracomponents/plasmaextracomponentsplugin.cpp
@@ -20,7 +20,7 @@
#include "plasmaextracomponentsplugin.h"
#include "appbackgroundprovider_p.h"
-//#include "resourceinstance.h"
+#include "plotter.h"
#include "fallbackcomponent.h"
#include <QtQml>
@@ -40,6 +40,8 @@
Q_ASSERT(uri == QLatin1String("org.kde.plasma.extras"));
//qmlRegisterType<ResourceInstance>(uri, 2, 0, "ResourceInstance");
qmlRegisterType<FallbackComponent>(uri, 2, 0, "FallbackComponent");
+ qmlRegisterType<PlotData>(uri, 2, 0, "PlotData");
+ qmlRegisterType<Plotter>(uri, 2, 0, "Plotter");
}
#include "plasmaextracomponentsplugin.moc"
diff --git a/src/declarativeimports/plasmaextracomponents/plotter.cpp b/src/declarativeimports/plasmaextracomponents/plotter.cpp
new file mode 100644
index 0000000..c5512b0
--- /dev/null
+++ b/src/declarativeimports/plasmaextracomponents/plotter.cpp
@@ -0,0 +1,711 @@
+/*
+ * This file is part of the KDE project
+ *
+ * Copyright © 2014 Fredrik Höglund <fredrik at kde.org>
+ * Copyright © 2014 Marco Martin <mart at kde.org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+*/
+
+#include <epoxy/gl.h>
+#include "plotter.h"
+
+#include <QGuiApplication>
+#include <QWindow>
+
+#include <QOpenGLContext>
+#include <QOpenGLShaderProgram>
+
+#include <QPainterPath>
+#include <QPolygonF>
+
+#include <QVector2D>
+#include <QMatrix4x4>
+
+#include <QSGTexture>
+#include <QSGSimpleTextureNode>
+
+#include <QQuickWindow>
+#include <QQuickView>
+#include <QQuickItem>
+
+#include <QDebug>
+
+//completely arbitrary
+static int s_defaultSampleSize = 40;
+
+PlotData::PlotData(QObject *parent)
+ : QObject(parent),
+ m_min(std::numeric_limits<qreal>::max()),
+ m_max(std::numeric_limits<qreal>::min()),
+ m_sampleSize(s_defaultSampleSize)
+{
+ m_values.reserve(s_defaultSampleSize);
+ for (int i = 0; i < s_defaultSampleSize; ++i) {
+ m_values << 0.0;
+ }
+}
+
+void PlotData::setColor(const QColor &color)
+{
+ if (m_color == color) {
+ return;
+ }
+
+ m_color = color;
+
+ emit colorChanged();
+}
+
+QColor PlotData::color() const
+{
+ return m_color;
+}
+
+qreal PlotData::max() const
+{
+ return m_max;
+}
+
+qreal PlotData::min() const
+{
+ return m_min;
+}
+
+void PlotData::setSampleSize(int size)
+{
+ if (m_sampleSize != size) {
+ return;
+ }
+
+ m_values.reserve(size);
+ if (m_values.size() > size) {
+ for (int i = 0; i < (m_values.size() - size); ++i) {
+ m_values.pop_front();
+ }
+ } else if (m_values.size() < size) {
+ for (int i = 0; i < (size - m_values.size()); ++i) {
+ m_values.prepend(0.0);
+ }
+ }
+
+ m_sampleSize = size;
+}
+
+QString PlotData::label() const
+{
+ return m_label;
+}
+
+void PlotData::setLabel(const QString &label)
+{
+ if (m_label == label) {
+ return;
+ }
+
+ m_label = label;
+ emit labelChanged();
+}
+
+void PlotData::addSample(qreal value)
+{
+
+ //assume at this point we'll have to pop a single time to stay in size
+ if (m_values.size() >= m_sampleSize) {
+ m_values.pop_front();
+ }
+
+ m_values.push_back(value);
+
+ m_max = std::numeric_limits<qreal>::min();
+ m_min = std::numeric_limits<qreal>::max();
+ for (auto v : m_values) {
+ if (v > m_max) {
+ m_max = v;
+ } else if (v < m_min) {
+ m_min = v;
+ }
+ }
+
+ emit valuesChanged();
+}
+
+QList<qreal> PlotData::values() const
+{
+ return m_values;
+}
+
+const char *vs_source =
+ "attribute vec4 vertex;\n"
+ "varying float gradient;\n"
+
+ "uniform mat4 matrix;\n"
+ "uniform float yMin;\n"
+ "uniform float yMax;\n"
+
+ "void main(void) {\n"
+ " gradient = (vertex.y - yMin) / (yMax - yMin);"
+ " gl_Position = matrix * vertex;\n"
+ "}";
+
+const char *fs_source=
+ "uniform vec4 color1;\n"
+ "uniform vec4 color2;\n"
+
+ "varying float gradient;\n"
+
+ "void main(void) {\n"
+ " gl_FragColor = mix(color1, color2, gradient);\n"
+ "}";
+
+
+
+
+// --------------------------------------------------
+
+
+
+class Texture : public QSGTexture
+{
+public:
+ Texture(QOpenGLContext *ctx);
+ ~Texture();
+
+ void bind() override final;
+ bool hasAlphaChannel() const override final { return true; }
+ bool hasMipmaps() const override final { return false; }
+ int textureId() const override final { return m_texture; }
+ QSize textureSize() const override final { return m_size; }
+
+ void recreate(const QSize &size);
+ GLuint fbo() const { return m_fbo; }
+
+private:
+ GLuint m_texture = 0;
+ GLuint m_fbo = 0;
+ GLenum m_internalFormat;
+ bool m_haveTexStorage;
+ QSize m_size;
+};
+
+Texture::Texture(QOpenGLContext *ctx) : QSGTexture()
+{
+ QPair<int, int> version = ctx->format().version();
+
+ if (ctx->isOpenGLES()) {
+ m_haveTexStorage = version >= qMakePair(3, 0) || ctx->hasExtension("GL_EXT_texture_storage");
+ m_internalFormat = version >= qMakePair(3, 0) ? GL_RGBA8 : GL_RGBA;
+ } else {
+ m_haveTexStorage = version >= qMakePair(4, 2) || ctx->hasExtension("GL_ARB_texture_storage");
+ m_internalFormat = GL_RGBA8;
+ }
+
+ glGenFramebuffers(1, &m_fbo);
+}
+
+Texture::~Texture()
+{
+ if (m_texture)
+ glDeleteTextures(1, &m_texture);
+
+ glDeleteFramebuffers(1, &m_fbo);
+}
+
+void Texture::bind()
+{
+ glBindTexture(GL_TEXTURE_2D, m_texture);
+}
+
+void Texture::recreate(const QSize &size)
+{
+ if (m_texture)
+ glDeleteTextures(1, &m_texture);
+
+ glGenTextures(1, &m_texture);
+ glBindTexture(GL_TEXTURE_2D, m_texture);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+
+ if (m_haveTexStorage)
+ glTexStorage2D(GL_TEXTURE_2D, 1, m_internalFormat, size.width(), size.height());
+ else
+ glTexImage2D(GL_TEXTURE_2D, 0, m_internalFormat, size.width(), size.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
+
+ m_size = size;
+}
+
+
+
+// ----------------------
+
+QOpenGLShaderProgram *Plotter::s_program = nullptr;
+int Plotter::u_matrix;
+int Plotter::u_color1;
+int Plotter::u_color2;
+int Plotter::u_yMin;
+int Plotter::u_yMax;
+
+Plotter::Plotter(QQuickItem *parent)
+ : QQuickItem(parent),
+ m_min(0),
+ m_max(0),
+ m_sampleSize(s_defaultSampleSize),
+ m_stacked(true),
+ m_autoRange(true)
+{
+ setFlag(ItemHasContents);
+}
+
+Plotter::~Plotter()
+{
+ delete m_node;
+}
+
+qreal Plotter::max() const
+{
+ return m_max;
+}
+
+qreal Plotter::min() const
+{
+ return m_min;
+}
+
+int Plotter::sampleSize() const
+{
+ return m_sampleSize;
+}
+
+void Plotter::setSampleSize(int size)
+{
+ if (m_sampleSize != size) {
+ return;
+ }
+
+ m_sampleSize = size;
+
+ for (auto data : m_plotData) {
+ data->setSampleSize(size);
+ }
+
+ update();
+ emit sampleSizeChanged();
+}
+
+bool Plotter::isStacked() const
+{
+ return m_stacked;
+}
+
+void Plotter::setStacked(bool stacked)
+{
+ if (m_stacked == stacked) {
+ return;
+ }
+
+ m_stacked = stacked;
+
+ emit stackedChanged();
+ update();
+}
+
+bool Plotter::isAutoRange() const
+{
+ return m_autoRange;
+}
+
+void Plotter::setAutoRange(bool autoRange)
+{
+ if (m_autoRange == autoRange) {
+ return;
+ }
+
+ m_autoRange = autoRange;
+
+ emit autoRangeChanged();
+ update();
+}
+
+void Plotter::addSample(const QList<qreal> &value)
+{
+ if (value.count() != m_plotData.count()) {
+ qWarning() << "Must add a new value per data set";
+ return;
+ }
+
+ int i = 0;
+ for (auto data : m_plotData) {
+ data->addSample(value.value(i));
+ ++i;
+ }
+
+ update();
+}
+
+void Plotter::dataSet_append(QQmlListProperty<PlotData> *list, PlotData *item)
+{
+ Plotter *p = static_cast<Plotter *>(list->object);
+ return p->m_plotData.append(item);
+}
+
+int Plotter::dataSet_count(QQmlListProperty<PlotData> *list)
+{
+ Plotter *p = static_cast<Plotter *>(list->object);
+ return p->m_plotData.count();
+}
+
+PlotData *Plotter::dataSet_at(QQmlListProperty<PlotData> *list, int index)
+{
+ Plotter *p = static_cast<Plotter *>(list->object);
+ return p->m_plotData.at(index);
+}
+
+void Plotter::dataSet_clear(QQmlListProperty<PlotData> *list)
+{
+ Plotter *p = static_cast<Plotter *>(list->object);
+ return p->m_plotData.clear();
+}
+
+
+QQmlListProperty<PlotData> Plotter::dataSets()
+{
+ return QQmlListProperty<PlotData>(this, 0, Plotter::dataSet_append, Plotter::dataSet_count, Plotter::dataSet_at, Plotter::dataSet_clear);
+}
+
+
+
+// Catmull-Rom interpolation
+QPainterPath Plotter::interpolate(const QVector<qreal> &p, qreal x0, qreal x1) const
+{
+ QPainterPath path;
+
+ const QMatrix4x4 matrix( 0, 1, 0, 0,
+ -1/6., 1, 1/6., 0,
+ 0, 1/6., 1, -1/6.,
+ 0, 0, 1, 0);
+
+ const qreal xDelta = (x1 - x0) / (p.count() - 3);
+ qreal x = x0 - xDelta;
+
+ path.moveTo(x0, p[0]);
+
+ for (int i = 1; i < p.count() - 2; i++) {
+ const QMatrix4x4 points(x, p[i-1], 0, 0,
+ x + xDelta * 1, p[i+0], 0, 0,
+ x + xDelta * 2, p[i+1], 0, 0,
+ x + xDelta * 3, p[i+2], 0, 0);
+
+ const QMatrix4x4 res = matrix * points;
+
+ path.cubicTo(res(1, 0), res(1, 1),
+ res(2, 0), res(2, 1),
+ res(3, 0), res(3, 1));
+
+ x += xDelta;
+ }
+
+ return path;
+}
+
+void Plotter::render()
+{
+ GLuint rb;
+
+ if (m_haveMSAA && m_haveFramebufferBlit) {
+ // Allocate a temporary MSAA renderbuffer
+ glGenRenderbuffers(1, &rb);
+ glBindRenderbuffer(GL_RENDERBUFFER, rb);
+ glRenderbufferStorageMultisample(GL_RENDERBUFFER, m_samples, m_internalFormat, width(), height());
+
+ // Attach it to the framebuffer object
+ glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb);
+ } else {
+ // If we don't have MSAA support we render directly into the texture
+ glBindFramebuffer(GL_FRAMEBUFFER, static_cast<Texture*>(m_node->texture())->fbo());
+ }
+
+ glViewport(0, 0, width(), height());
+
+ // Clear the color buffer
+ glClearColor(0.0, 0.0, 0.0, 0.0);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Add horizontal lines
+ int lineCount = height() / 20;
+
+ QVector<QVector2D> vertices;
+ for (int i = 0; i < lineCount; i++)
+ vertices << QVector2D(0, i * 20) << QVector2D(width(), i * 20);
+
+ // Tessellate
+ float min = height();
+ float max = height();
+
+ QHash<PlotData *, int> verticesCounts;
+ for (auto data : m_plotData) {
+ // Interpolate the data set
+ const QPainterPath path = interpolate(data->m_normalizedValues, 0, width());
+
+ // Flatten the path
+ const QList<QPolygonF> polygons = path.toSubpathPolygons();
+
+ for (const QPolygonF &p : polygons) {
+ verticesCounts[data] = 0;
+ vertices << QVector2D(p.first().x(), height());
+
+ for (int i = 0; i < p.count()-1; i++) {
+ min = qMin<float>(min, height() - p[i].y());
+ vertices << QVector2D(p[i].x(), height() - p[i].y());
+ vertices << QVector2D((p[i].x() + p[i+1].x()) / 2.0, height());
+ verticesCounts[data] += 2;
+ }
+
+ min = qMin<float>(min, height() - p.last().y());
+ vertices << QVector2D(p.last().x(), height() - p.last().y());
+ vertices << QVector2D(p.last().x(), height());
+ verticesCounts[data] += 3;
+ }
+ }
+
+ // Upload vertices
+ GLuint vbo;
+ glGenBuffers(1, &vbo);
+
+ glBindBuffer(GL_ARRAY_BUFFER, vbo);
+ glBufferData(GL_ARRAY_BUFFER, vertices.count() * sizeof(QVector2D), vertices.constData(), GL_STATIC_DRAW);
+
+ // Set up the array
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(QVector2D), nullptr);
+ glEnableVertexAttribArray(0);
+
+ // Bind the shader program
+ s_program->bind();
+ s_program->setUniformValue(u_matrix, m_matrix);
+
+ // Draw the lines
+ s_program->setUniformValue(u_yMin, (float) 0.0);
+ s_program->setUniformValue(u_yMax, (float) height());
+ s_program->setUniformValue(u_color1, QColor(230, 230, 230));
+ s_program->setUniformValue(u_color2, QColor(250, 250, 250));
+
+ glDrawArrays(GL_LINES, 0, lineCount * 2);
+
+ // Enable alpha blending
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ int oldCount = 0;
+ for (auto data : m_plotData) {
+ QColor color2 = data->color();
+ color2.setAlphaF(0.60);
+ // Draw the graph
+ s_program->setUniformValue(u_yMin, min);
+ s_program->setUniformValue(u_yMax, max);
+ s_program->setUniformValue(u_color1, data->color());
+ s_program->setUniformValue(u_color2, color2);
+
+ glDrawArrays(GL_TRIANGLE_STRIP, lineCount * 2 + oldCount, verticesCounts[data]);
+ oldCount = verticesCounts[data];
+ }
+
+ glDisable(GL_BLEND);
+
+ if (m_haveMSAA && m_haveFramebufferBlit) {
+ // Resolve the MSAA buffer
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, m_fbo);
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<Texture*>(m_node->texture())->fbo());
+ glBlitFramebuffer(0, 0, width(), height(), 0, 0, width(), height(), GL_COLOR_BUFFER_BIT, GL_NEAREST);
+
+ // Delete the render buffer
+ glDeleteRenderbuffers(1, &rb);
+ }
+
+ // Delete the VBO
+ glDeleteBuffers(1, &vbo);
+}
+
+QSGNode *Plotter::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData)
+{
+ Q_UNUSED(updatePaintNodeData)
+
+ QSGSimpleTextureNode *n = static_cast<QSGSimpleTextureNode *>(oldNode);
+
+ if (!n) {
+ n = new QSGSimpleTextureNode();
+ n->setTexture(new Texture(window()->openglContext()));
+ n->setFiltering(QSGTexture::Linear);
+
+ m_node = n;
+ }
+
+ if (!m_initialized) {
+ glGenFramebuffers(1, &m_fbo);
+ connect(window(), &QQuickWindow::beforeRendering, this, &Plotter::render, Qt::DirectConnection);
+
+ QOpenGLContext *ctx = window()->openglContext();
+ QPair<int, int> version = ctx->format().version();
+
+ if (ctx->isOpenGLES()) {
+ m_haveMSAA = version >= qMakePair(3, 0) || ctx->hasExtension("GL_NV_framebuffer_multisample");
+ m_haveFramebufferBlit = version >= qMakePair(3, 0) || ctx->hasExtension("GL_NV_framebuffer_blit");
+ m_haveInternalFormatQuery = version >= qMakePair(3, 0);
+ m_internalFormat = version >= qMakePair(3, 0) ? GL_RGBA8 : GL_RGBA;
+ } else {
+ m_haveMSAA = version >= qMakePair(3, 2) || ctx->hasExtension("GL_ARB_framebuffer_object") ||
+ ctx->hasExtension("GL_EXT_framebuffer_multisample");
+ m_haveFramebufferBlit = version >= qMakePair(3, 0) || ctx->hasExtension("GL_ARB_framebuffer_object") ||
+ ctx->hasExtension("GL_EXT_framebuffer_blit");
+ m_haveInternalFormatQuery = version >= qMakePair(4, 2) || ctx->hasExtension("GL_ARB_internalformat_query");
+ m_internalFormat = GL_RGBA8;
+ }
+
+ // Query the maximum sample count for the internal format
+ if (m_haveInternalFormatQuery) {
+ int count = 0;
+ glGetInternalformativ(GL_RENDERBUFFER, m_internalFormat, GL_NUM_SAMPLE_COUNTS, 1, &count);
+
+ if (count > 0) {
+ QVector<int> samples(count);
+ glGetInternalformativ(GL_RENDERBUFFER, m_internalFormat, GL_SAMPLES, count, samples.data());
+
+ // The samples are returned in descending order. Choose the highest value.
+ m_samples = samples.at(0);
+ } else {
+ m_samples = 0;
+ }
+ } else if (m_haveMSAA) {
+ glGetIntegerv(GL_MAX_SAMPLES, &m_samples);
+ } else {
+ m_samples = 0;
+ }
+
+ m_initialized = true;
+ }
+
+ if (!s_program) {
+ s_program = new QOpenGLShaderProgram;
+ s_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vs_source);
+ s_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fs_source);
+ s_program->bindAttributeLocation("vertex", 0);
+ s_program->link();
+
+ u_yMin = s_program->uniformLocation("yMin");
+ u_yMax = s_program->uniformLocation("yMax");
+ u_color1 = s_program->uniformLocation("color1");
+ u_color2 = s_program->uniformLocation("color2");
+ u_matrix = s_program->uniformLocation("matrix");
+ }
+
+ if (n->texture()->textureSize() != boundingRect().size()) {
+ static_cast<Texture *>(n->texture())->recreate(boundingRect().size().toSize());
+ m_matrix = QMatrix4x4();
+ m_matrix.ortho(0, width(), 0, height(), -1, 1);
+ }
+
+ //normalize data
+ m_max = std::numeric_limits<qreal>::min();
+ m_min = std::numeric_limits<qreal>::max();
+ qreal adjustedMax = m_max;
+ qreal adjustedMin = m_min;
+ if (m_stacked) {
+ PlotData *previousData = 0;
+ QList<PlotData *>::const_iterator i = m_plotData.constEnd();
+ do {
+ --i;
+ PlotData *data = *i;
+ data->m_normalizedValues.clear();
+ data->m_normalizedValues.resize(data->values().count());
+ if (previousData) {
+ for (int i = 0; i < data->values().count(); ++i) {
+ data->m_normalizedValues[i] = data->values().value(i) + previousData->m_normalizedValues.value(i);
+
+ if (data->m_normalizedValues[i] > adjustedMax) {
+ adjustedMax = data->m_normalizedValues[i];
+ }
+ if (data->m_normalizedValues[i] < adjustedMin) {
+ adjustedMin = data->m_normalizedValues[i];
+ }
+ }
+ } else {
+ data->m_normalizedValues = data->values().toVector();
+ if (data->max() > adjustedMax) {
+ adjustedMax = data->max();
+ }
+ if (data->min() < adjustedMin) {
+ adjustedMin = data->min();
+ }
+ }
+ previousData = data;
+
+ //global max and global min
+ if (data->max() > m_max) {
+ m_max = data->max();
+ }
+ if (data->min() < m_min) {
+ m_min = data->min();
+ }
+ } while (i != m_plotData.constBegin());
+
+ } else {
+ for (auto data : m_plotData) {
+ data->m_normalizedValues.clear();
+ data->m_normalizedValues = data->values().toVector();
+ //global max and global min
+ if (data->max() > m_max) {
+ adjustedMax = m_max = data->max();
+ }
+ if (data->min() < m_min) {
+ adjustedMin = m_min = data->min();
+ }
+ }
+ }
+
+ if (m_autoRange) {
+ //leave some empty space (of a line) top and bottom
+ adjustedMax += height()/20;
+ adjustedMin -= height()/20;
+ qreal adjust;
+ //this should never happen, remove?
+ if (qFuzzyCompare(adjustedMax - adjustedMin, 0)) {
+ adjust = 1;
+ } else {
+ adjust = (height() / (adjustedMax - adjustedMin));
+ }
+ //normalizebased on global max and min
+ for (auto data : m_plotData) {
+ for (int i = 0; i < data->values().count(); ++i) {
+ data->m_normalizedValues[i] = (data->m_normalizedValues.value(i) - adjustedMin) * adjust;
+ }
+ }
+ }
+
+ if (window()) {
+ window()->update();
+ }
+
+ n->setRect(boundingRect());
+ return n;
+}
+
+
+#include "plotter.moc"
diff --git a/src/declarativeimports/plasmaextracomponents/plotter.h b/src/declarativeimports/plasmaextracomponents/plotter.h
new file mode 100644
index 0000000..983dd10
--- /dev/null
+++ b/src/declarativeimports/plasmaextracomponents/plotter.h
@@ -0,0 +1,192 @@
+/*
+ * This file is part of the KDE project
+ *
+ * Copyright © 2014 Fredrik Höglund <fredrik at kde.org>
+ * Copyright © 2014 Marco Martin <mart at kde.org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+#include < * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+*/
+
+#ifndef PLASMA_PLOTTER_H
+#define PLASMA_PLOTTER_H
+
+#include <QSGTexture>
+#include <QSGSimpleTextureNode>
+#include <QQuickItem>
+#include <QQmlListProperty>
+
+class PlotData : public QObject
+{
+ Q_OBJECT
+ /**
+ * text Label of the data set: note this is purely a model, it will need a Label somewhere to be actually painted
+ */
+ Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)
+
+ /**
+ * Color to plot this data set
+ */
+ Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
+
+ /**
+ * All the values currently in this data set
+ */
+ Q_PROPERTY(QList<qreal> values READ values NOTIFY valuesChanged)
+
+ /**
+ * Maximum value of this data set
+ */
+ Q_PROPERTY(qreal max READ max NOTIFY maxChanged)
+
+ /**
+ * Minimum value of this data set
+ */
+ Q_PROPERTY(qreal min READ min NOTIFY minChanged)
+
+public:
+ PlotData(QObject *parent = 0);
+
+ void setColor(const QColor &color);
+ QColor color() const;
+
+ void addSample(qreal value);
+
+ QList<qreal> values() const;
+
+ QVector<qreal> m_normalizedValues;
+
+ qreal max() const;
+ qreal min() const;
+
+ void setSampleSize(int size);
+
+ QString label() const;
+ void setLabel(const QString &label);
+
+Q_SIGNALS:
+ void colorChanged();
+ void valuesChanged();
+ void maxChanged();
+ void minChanged();
+ void labelChanged();
+
+private:
+ QString m_label;
+ QColor m_color;
+ QList<qreal> m_values;
+
+ qreal m_min;
+ qreal m_max;
+ int m_sampleSize;
+};
+
+class Plotter : public QQuickItem
+{
+ Q_OBJECT
+ Q_PROPERTY(QQmlListProperty<PlotData> dataSets READ dataSets)
+
+ /**
+ * maximum value among all graphs
+ */
+ Q_PROPERTY(qreal max READ max NOTIFY maxChanged)
+
+ /**
+ * minimum value among all graphs
+ */
+ Q_PROPERTY(qreal min READ min NOTIFY minChanged)
+
+ /**
+ * draw at most n samples, if new samples are pushed old values are started to be thrown away
+ */
+ Q_PROPERTY(int sampleSize READ sampleSize WRITE setSampleSize NOTIFY sampleSizeChanged)
+
+ /**
+ * if true stack the graphs one on top of each other instead of just painting one on top of each other
+ */
+ Q_PROPERTY(bool stacked READ isStacked WRITE setStacked NOTIFY stackedChanged)
+
+ /**
+ * If true, the graph is automatically scaled to always fit in the Plotter area
+ */
+ Q_PROPERTY(bool autoRange READ isAutoRange WRITE setAutoRange NOTIFY autoRangeChanged)
+
+ //Q_CLASSINFO("DefaultProperty", "dataSets")
+
+public:
+ Plotter(QQuickItem *parent = 0);
+ ~Plotter();
+
+ qreal max() const;
+ qreal min() const;
+
+ int sampleSize() const;
+ void setSampleSize(int size);
+
+ bool isStacked() const;
+ void setStacked(bool stacked);
+
+ bool isAutoRange() const;
+ void setAutoRange(bool autorange);
+
+ QQmlListProperty<PlotData> dataSets();
+ static void dataSet_append(QQmlListProperty<PlotData> *list, PlotData *item);
+ static int dataSet_count(QQmlListProperty<PlotData> *list);
+ static PlotData *dataSet_at(QQmlListProperty<PlotData> *list, int pos);
+ static void dataSet_clear(QQmlListProperty<PlotData> *list);
+
+ Q_INVOKABLE void addSample(const QList<qreal> &value);
+private:
+ QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) override final;
+ QPainterPath interpolate(const QVector<qreal> &p, qreal x0, qreal x1) const;
+
+Q_SIGNALS:
+ void maxChanged();
+ void minChanged();
+ void sampleSizeChanged();
+ void stackedChanged();
+ void autoRangeChanged();
+
+private Q_SLOTS:
+ void render();
+
+private:
+ QList<PlotData *> m_plotData;
+
+ GLuint m_fbo = 0;
+ QSGSimpleTextureNode *m_node = nullptr;
+ qreal m_min;
+ qreal m_max;
+ int m_sampleSize;
+ bool m_stacked;
+ bool m_autoRange;
+
+ QMatrix4x4 m_matrix;
+ bool m_initialized = false;
+ bool m_haveMSAA;
+ bool m_haveFramebufferBlit;
+ bool m_haveInternalFormatQuery;
+ GLenum m_internalFormat;
+ int m_samples;
+
+ static QOpenGLShaderProgram *s_program;
+ static int u_matrix;
+ static int u_color1;
+ static int u_color2;
+ static int u_yMin;
+ static int u_yMax;
+};
+
+#endif
diff --git a/tests/plotter.qml b/tests/plotter.qml
new file mode 100644
index 0000000..4d938c0
--- /dev/null
+++ b/tests/plotter.qml
@@ -0,0 +1,48 @@
+import QtQuick 2.0
+
+import org.kde.plasma.core 2.0
+import org.kde.plasma.components 2.0
+import org.kde.plasma.extras 2.0
+
+Item {
+ width: 500
+ height: 200
+
+ Plotter {
+ id: renderer
+ anchors.fill: parent
+ anchors.margins: 0
+ stacked: stackedButton.checked
+ autoRange: autoRangeButton.checked
+
+ dataSets: [
+ PlotData {
+ color: "#4cb2ff"
+ },
+ PlotData {
+ color: "#00b200"
+ }
+ ]
+
+ }
+ Row {
+ Button {
+ text: "Add value"
+ onClicked: {
+ renderer.addSample([Math.random() * 40, Math.random() * 40])
+ }
+ }
+ ToolButton {
+ id: stackedButton
+ text: "Stacked"
+ checkable: true
+ checked: true
+ }
+ ToolButton {
+ id: autoRangeButton
+ text: "Auto Range"
+ checkable: true
+ checked: true
+ }
+ }
+}
--
To view, visit https://gerrit.vesnicky.cesnet.cz/r/244
To unsubscribe, visit https://gerrit.vesnicky.cesnet.cz/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I4a29a25412b375dfbd7f0db618040bae19c9b39c
Gerrit-PatchSet: 1
Gerrit-Project: plasma-framework
Gerrit-Branch: master
Gerrit-Owner: Marco Martin <notmart at gmail.com>
Gerrit-Reviewer: Martin Klapetek <mklapetek at kde.org>
Gerrit-Reviewer: Sebastian Kügler <sebas at kde.org>
More information about the Plasma-devel
mailing list