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 <fred...@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 <fred...@kde.org> + * Copyright © 2014 Marco Martin <m...@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 <fred...@kde.org> + * Copyright © 2014 Marco Martin <m...@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 <notm...@gmail.com> Gerrit-Reviewer: Martin Klapetek <mklape...@kde.org> Gerrit-Reviewer: Sebastian Kügler <se...@kde.org> _______________________________________________ Plasma-devel mailing list Plasma-devel@kde.org https://mail.kde.org/mailman/listinfo/plasma-devel