diff options
author | Samuel Fadel <samuelfadel@gmail.com> | 2015-09-29 14:59:59 -0300 |
---|---|---|
committer | Samuel Fadel <samuelfadel@gmail.com> | 2015-09-29 14:59:59 -0300 |
commit | 30f327b2fd25104916bffe6fe76823f0dbe35a72 (patch) | |
tree | 3ceb449cbd3b6e8965e891efe3ea6b36183a6b10 | |
parent | 56c9ebb2e41bd0487199ed95838cd9e1c1d9dd8d (diff) |
Inital history graph implementation and using linear scales where applicable.
- geometry.h for geometry calculation functions
- scale.h for implementations of scales (currently only the linear scale)
- updated main_view.qml for the new HistoryGraph component
- HistoryGraph displays each subsample used as a mini scatterplot (no colors
currently)
- Scatterplot now uses scale.h for transformations
- Code cleanup and some bug fixes
-rw-r--r-- | geometry.cpp | 39 | ||||
-rw-r--r-- | geometry.h | 13 | ||||
-rw-r--r-- | historygraph.cpp | 260 | ||||
-rw-r--r-- | historygraph.h | 16 | ||||
-rw-r--r-- | main.cpp | 33 | ||||
-rw-r--r-- | main_view.qml | 4 | ||||
-rw-r--r-- | pm.pro | 3 | ||||
-rw-r--r-- | scale.h | 81 | ||||
-rw-r--r-- | scatterplot.cpp | 133 | ||||
-rw-r--r-- | scatterplot.h | 7 |
10 files changed, 495 insertions, 94 deletions
diff --git a/geometry.cpp b/geometry.cpp new file mode 100644 index 0000000..3a4a861 --- /dev/null +++ b/geometry.cpp @@ -0,0 +1,39 @@ +#include "geometry.h" + +static const float PI = 3.1415f; + +int calculateCircleVertexCount(float radius) +{ + // 10 * sqrt(r) \approx 2*pi / acos(1 - 1 / (4*r)) + return int(10.0 * sqrt(radius)); +} + +void updateCircleGeometry(QSGGeometry *geometry, float radius, float cx, float cy) +{ + int vertexCount = geometry->vertexCount(); + + float theta = 2 * PI / float(vertexCount); + float c = cosf(theta); + float s = sinf(theta); + float x = radius; + float y = 0; + + QSGGeometry::Point2D *vertexData = geometry->vertexDataAsPoint2D(); + for (int i = 0; i < vertexCount; i++) { + vertexData[i].set(x + cx, y + cy); + + float t = x; + x = c*x - s*y; + y = s*t + c*y; + } +} + +void updateRectGeometry(QSGGeometry *geometry, float x, float y, float w, float h) +{ + QSGGeometry::Point2D *vertexData = geometry->vertexDataAsPoint2D(); + + vertexData[0].set(x, y); + vertexData[1].set(x + w, y); + vertexData[2].set(x + w, y + h); + vertexData[3].set(x, y + h); +} diff --git a/geometry.h b/geometry.h new file mode 100644 index 0000000..b29e41f --- /dev/null +++ b/geometry.h @@ -0,0 +1,13 @@ +#ifndef GEOMETRY_H +#define GEOMETRY_H + +#include <QSGGeometry> + +// Circle +int calculateCircleVertexCount(float radius); +void updateCircleGeometry(QSGGeometry *geometry, float radius, float cx, float cy); + +// Rect +void updateRectGeometry(QSGGeometry *geometry, float x, float y, float w, float h); + +#endif // GEOMETRY_H diff --git a/historygraph.cpp b/historygraph.cpp index 1a4532a..997d079 100644 --- a/historygraph.cpp +++ b/historygraph.cpp @@ -1,14 +1,268 @@ #include "historygraph.h" +#include <algorithm> +#include <functional> +#include <numeric> +#include <utility> + +#include "geometry.h" +#include "scale.h" + +static const float ASPECT = 4.f / 3.f; +static const float MARGIN = 0.1f; +static const float GLYPH_SIZE = 4.0f; +static const float PADDING = 0.05f; + +class HistoryGraph::HistoryItemNode +{ +public: + HistoryItemNode(const arma::mat &item); + ~HistoryItemNode(); + + void append(HistoryItemNode *node); + const QList<HistoryItemNode *> &children() const { return m_next; } + const arma::mat &item() const; + int depth() const { return m_depth; } + int breadth() const { return m_breadth; } + void updateDepth(); + void updateBreadth(); + + static int treeBreadth(HistoryItemNode *node, int level); + +private: + arma::mat m_item; + HistoryItemNode *m_prev; + QList<HistoryItemNode *> m_next; + int m_depth, m_breadth; +}; + +HistoryGraph::HistoryItemNode::HistoryItemNode(const arma::mat &item) + : m_item(item) + , m_prev(0) + , m_depth(1) + , m_breadth(1) +{ +} + +HistoryGraph::HistoryItemNode::~HistoryItemNode() +{ + m_prev = 0; + + while (!m_next.isEmpty()) { + delete m_next.takeLast(); + } +} + +void HistoryGraph::HistoryItemNode::updateDepth() +{ + HistoryGraph::HistoryItemNode *node = *std::max_element( + m_next.cbegin(), m_next.cend(), + [](const HistoryGraph::HistoryItemNode *n1, const HistoryGraph::HistoryItemNode *n2) { + return n1->depth() < n2->depth(); + }); + + m_depth = 1 + node->depth(); + + if (m_prev) { + m_prev->updateDepth(); + } +} + +int HistoryGraph::HistoryItemNode::treeBreadth(HistoryGraph::HistoryItemNode *node, int level) { + if (!node || level < 1) { + return 0; + } + + if (level == 1) { + return 1; + } + + return std::accumulate(node->m_next.cbegin(), node->m_next.cend(), 0, [](int b, HistoryGraph::HistoryItemNode *n) { + n->m_breadth = treeBreadth(n, n->depth()); + return b + n->m_breadth; + }); +} + +void HistoryGraph::HistoryItemNode::updateBreadth() +{ + HistoryItemNode *node = this; + while (node->m_prev) { + node = node->m_prev; + } + + node->m_breadth = treeBreadth(node, node->m_depth); +} + +void HistoryGraph::HistoryItemNode::append(HistoryItemNode *node) +{ + if (!node) { + return; + } + + m_next.append(node); + if (node->depth() + 1 > m_depth) { + updateDepth(); + } + + updateBreadth(); + + node->m_prev = this; +} + +const arma::mat &HistoryGraph::HistoryItemNode::item() const +{ + return m_item; +} + HistoryGraph::HistoryGraph(QQuickItem *parent) : QQuickItem(parent) + , m_firstNode(0) + , m_currentNode(0) + , m_needsUpdate(false) { + setClip(true); + setFlag(QQuickItem::ItemHasContents); } -void HistoryGraph::addHistoryItem(const int &item) -{} +HistoryGraph::~HistoryGraph() +{ + delete m_firstNode; + m_firstNode = 0; +} + +void HistoryGraph::addHistoryItem(const arma::mat &item) +{ + HistoryItemNode *newNode = new HistoryItemNode(item); + if (m_currentNode) { + m_currentNode->append(newNode); + } else { + m_firstNode = newNode; + } + + m_currentNode = newNode; + m_needsUpdate = true; + update(); +} + +void HistoryGraph::addScatterplot(QSGNode *node, const HistoryGraph::HistoryItemNode *historyItemNode, float x, float y, float w, float h) +{ + const arma::mat &xy = historyItemNode->item(); + int vertexCount = calculateCircleVertexCount(GLYPH_SIZE / 2); + + LinearScale sx(xy.col(0).min(), xy.col(0).max(), x, x + w); + LinearScale sy(xy.col(1).min(), xy.col(1).max(), y + h, y); // reverse on purpose + + for (arma::uword i = 0; i < xy.n_rows; i++) { + const arma::rowvec &row = xy.row(i); + QSGGeometryNode *glyphNode = new QSGGeometryNode; + + QSGGeometry *glyphGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); + glyphGeometry->setDrawingMode(GL_POLYGON); + updateCircleGeometry(glyphGeometry, GLYPH_SIZE / 2 - 0.5, sx(row[0]), sy(row[1])); + glyphNode->setGeometry(glyphGeometry); + glyphNode->setFlag(QSGNode::OwnsGeometry); + + QSGFlatColorMaterial *material = new QSGFlatColorMaterial; + material->setColor(QColor()); + glyphNode->setMaterial(material); + glyphNode->setFlag(QSGNode::OwnsMaterial); + + // Place the glyph geometry node under an opacity node + QSGOpacityNode *glyphOpacityNode = new QSGOpacityNode; + //glyphOpacityNode->appendChildNode(glyphOutlineNode); + glyphOpacityNode->appendChildNode(glyphNode); + node->appendChildNode(glyphOpacityNode); + } +} + +QSGNode *HistoryGraph::createNodeTree() +{ + if (!m_firstNode) { + return 0; + } + + //int breadth = m_firstNode->breadth(); + //int depth = m_firstNode->depth(); + + QSGNode *sceneGraphRoot = new QSGNode; + HistoryItemNode *histNode = m_firstNode; + float margin = height()*MARGIN; + float padding = height()*PADDING; + float h = height() - 2.f*margin; + float w = ASPECT * h; + float x = margin; + + QMatrix4x4 mat; + do { + QSGOpacityNode *opacityNode = new QSGOpacityNode; + opacityNode->setOpacity(histNode == m_currentNode ? 1.0f : 0.4f); + + QSGGeometryNode *histItemGeomNode = new QSGGeometryNode; + QSGGeometry *histItemGeom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4); + updateRectGeometry(histItemGeom, x, margin, w, h); + histItemGeom->setDrawingMode(GL_LINE_LOOP); + histItemGeomNode->setGeometry(histItemGeom); + histItemGeomNode->setFlag(QSGNode::OwnsGeometry); + + QSGFlatColorMaterial *material = new QSGFlatColorMaterial; + material->setColor(QColor()); + histItemGeomNode->setMaterial(material); + histItemGeomNode->setFlag(QSGNode::OwnsMaterial); + + addScatterplot(histItemGeomNode, histNode, x + padding, margin + padding, w - 2*padding, h - 2*padding); + opacityNode->appendChildNode(histItemGeomNode); + sceneGraphRoot->appendChildNode(opacityNode); + + x += w + 2.f*margin; + + QList<HistoryItemNode *> children = histNode->children(); + if (children.isEmpty()) { + break; + } + + // FIXME: add children + histNode = children[0]; + } while (true); + + // setWidth(xPos + radius + margin); + + return sceneGraphRoot; +} + +void HistoryGraph::updateNodeTree(QSGNode *root) +{ + if (!m_firstNode) { + return; + } + + // FIXME: (really) lame update algorithm + + QSGNode *child = root->firstChild(); + if (child) { + root->removeChildNode(child); + delete child; + } + + root->appendChildNode(createNodeTree()); +} QSGNode *HistoryGraph::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { - return oldNode; + QSGNode *root = 0; + if (!oldNode) { + root = new QSGNode; + QSGNode *node = createNodeTree(); + if (node) { + root->appendChildNode(node); + } + } else { + root = oldNode; + } + + if (m_needsUpdate) { + m_needsUpdate = false; + updateNodeTree(root); + } + + return root; } diff --git a/historygraph.h b/historygraph.h index b854d1e..564845c 100644 --- a/historygraph.h +++ b/historygraph.h @@ -1,20 +1,32 @@ #ifndef HISTORYGRAPH_H #define HISTORYGRAPH_H -#include <armadillo> #include <QtQuick> +#include <armadillo> class HistoryGraph : public QQuickItem { Q_OBJECT public: HistoryGraph(QQuickItem *parent = 0); + ~HistoryGraph(); public slots: - void addHistoryItem(const int &item); + void addHistoryItem(const arma::mat &item); protected: QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *); + +private: + class HistoryItemNode; + + QSGNode *createNodeTree(); + void updateNodeTree(QSGNode *root); + void addScatterplot(QSGNode *node, const HistoryItemNode *historyItemNode, float x, float y, float w, float h); + + + HistoryItemNode *m_firstNode, *m_currentNode; + bool m_needsUpdate; }; #endif // HISTORYGRAPH_H @@ -8,6 +8,7 @@ #include "mp.h" #include "continuouscolorscale.h" #include "scatterplot.h" +#include "historygraph.h" #include "interactionhandler.h" #include "selectionhandler.h" #include "effectivenessobserver.h" @@ -19,6 +20,7 @@ int main(int argc, char **argv) QApplication app(argc, argv); qmlRegisterType<Scatterplot>("PM", 1, 0, "Scatterplot"); + qmlRegisterType<HistoryGraph>("PM", 1, 0, "HistoryGraph"); // Set up multisampling QSurfaceFormat fmt; @@ -42,7 +44,6 @@ int main(int argc, char **argv) arma::mat Ys(subsampleSize, 2, arma::fill::randn); Ys = mp::forceScheme(mp::dist(X.rows(sampleIndices)), Ys); - /* ColorScale colorScale{ QColor("#1f77b4"), QColor("#ff7f0e"), @@ -55,14 +56,13 @@ int main(int argc, char **argv) QColor("#7f7f7f"), }; colorScale.setExtents(labels.min(), labels.max()); - */ - ContinuousColorScale colorScale = ContinuousColorScale::builtin(ContinuousColorScale::RED_GRAY_BLUE); - colorScale.setExtents(-1, 1); + //ContinuousColorScale colorScale = ContinuousColorScale::builtin(ContinuousColorScale::RED_GRAY_BLUE); + //colorScale.setExtents(-1, 1); Scatterplot *subsamplePlot = engine.rootObjects()[0]->findChild<Scatterplot *>("subsamplePlot"); + HistoryGraph *history = engine.rootObjects()[0]->findChild<HistoryGraph *>("history"); subsamplePlot->setAcceptedMouseButtons(Qt::LeftButton | Qt::MiddleButton | Qt::RightButton); - subsamplePlot->setXY(Ys); - subsamplePlot->setColorData(arma::zeros<arma::vec>(subsampleSize)); + // subsamplePlot->setColorData(arma::zeros<arma::vec>(subsampleSize)); subsamplePlot->setColorScale(&colorScale); Scatterplot *plot = engine.rootObjects()[0]->findChild<Scatterplot *>("plot"); @@ -72,13 +72,16 @@ int main(int argc, char **argv) &interactionHandler, SLOT(setSubsample(const arma::mat &))); QObject::connect(&interactionHandler, SIGNAL(subsampleChanged(const arma::mat &)), plot, SLOT(setXY(const arma::mat &))); + QObject::connect(subsamplePlot, SIGNAL(xyChanged(const arma::mat &)), + history, SLOT(addHistoryItem(const arma::mat &))); SelectionHandler selectionHandler(sampleIndices); - QObject::connect(subsamplePlot, SIGNAL(selectionChanged(const arma::uvec &)), - &selectionHandler, SLOT(setSelection(const arma::uvec &))); - QObject::connect(&selectionHandler, SIGNAL(selectionChanged(const arma::uvec &)), - plot, SLOT(setSelection(const arma::uvec &))); + QObject::connect(subsamplePlot, SIGNAL(selectionChanged(const QSet<int> &)), + &selectionHandler, SLOT(setSelection(const QSet<int> &))); + QObject::connect(&selectionHandler, SIGNAL(selectionChanged(const QSet<int> &)), + plot, SLOT(setSelection(const QSet<int> &))); + /* DistortionObserver distortionObs(X, sampleIndices); std::unique_ptr<DistortionMeasure> distortionMeasure(new NPDistortion()); distortionObs.setMeasure(distortionMeasure.get()); @@ -94,11 +97,19 @@ int main(int argc, char **argv) &enforcer, SLOT(setMeasureDifference(const arma::vec &))); QObject::connect(&enforcer, SIGNAL(effectivenessChanged(const arma::vec &)), subsamplePlot, SLOT(setColorData(const arma::vec &))); + */ + /* ContinuousColorScale ccolorScale = ContinuousColorScale::builtin(ContinuousColorScale::RED_GRAY_BLUE); ccolorScale.setExtents(-1, 1); plot->setColorScale(&ccolorScale); - interactionHandler.setSubsample(Ys); + */ + plot->setColorScale(&colorScale); + plot->setColorData(labels); + + //interactionHandler.setSubsample(Ys); + subsamplePlot->setXY(Ys); + subsamplePlot->setColorData(labels(sampleIndices)); return app.exec(); } diff --git a/main_view.qml b/main_view.qml index 4c349f9..49448cb 100644 --- a/main_view.qml +++ b/main_view.qml @@ -77,9 +77,11 @@ ApplicationWindow { } } - ScrollView { + Rectangle { Layout.fillWidth: true Layout.minimumHeight: 150 + border.width: 1 + border.color: "#cccccc" HistoryGraph { id: history @@ -4,6 +4,8 @@ QMAKE_CXXFLAGS += -std=c++11 -fopenmp QMAKE_LIBS += -larmadillo -fopenmp HEADERS += colorscale.h \ continuouscolorscale.h \ + geometry.h \ + scale.h \ scatterplot.h \ historygraph.h \ interactionhandler.h \ @@ -16,6 +18,7 @@ HEADERS += colorscale.h \ SOURCES += main.cpp \ colorscale.cpp \ continuouscolorscale.cpp \ + geometry.cpp \ scatterplot.cpp \ historygraph.cpp \ interactionhandler.cpp \ @@ -0,0 +1,81 @@ +#ifndef SCALE_H +#define SCALE_H + +#include <algorithm> + +class Scale +{ +public: + Scale(float domainMin, float domainMax, float rangeMin, float rangeMax) + : m_domainMin(domainMin) + , m_domainMax(domainMax) + , m_rangeMin(rangeMin) + , m_rangeMax(rangeMax) + { + valuesUpdated(); + } + + virtual ~Scale() {} + + void setRangeMin(float rangeMin) { m_rangeMin = rangeMin; valuesUpdated(); } + void setRangeMax(float rangeMax) { m_rangeMax = rangeMax; valuesUpdated(); } + void setDomainMin(float domainMin) { m_domainMin = domainMin; valuesUpdated(); } + void setDomainMax(float domainMax) { m_domainMax = domainMax; valuesUpdated(); } + + void setRange(float rangeMin, float rangeMax) + { + m_rangeMin = rangeMin; + m_rangeMax = rangeMax; + valuesUpdated(); + } + + void setDomain(float domainMin, float domainMax) + { + m_domainMin = domainMin; + m_domainMax = domainMax; + valuesUpdated(); + } + + void reverse() + { + std::swap(m_rangeMin, m_domainMin); + std::swap(m_rangeMax, m_domainMax); + valuesUpdated(); + } + + virtual float operator()(float value) const = 0; + +protected: + // Called when internal values change + virtual void valuesUpdated() {} + + float m_domainMin, m_domainMax; + float m_rangeMin, m_rangeMax; +}; + +class LinearScale + : public Scale +{ +public: + LinearScale(float domainMin, float domainMax, float rangeMin, float rangeMax) + : Scale(domainMin, domainMax, rangeMin, rangeMax) + { + valuesUpdated(); + } + + virtual float operator()(float value) const + { + return (value - m_domainMin) * m_transformSlope + m_rangeMin; + } + +protected: + virtual void valuesUpdated() + { + m_transformSlope = (m_rangeMax - m_rangeMin) / (m_domainMax - m_domainMin); + } + +private: + float m_transformSlope; +}; + +#endif // SCALE_H diff --git a/scatterplot.cpp b/scatterplot.cpp index c5348f2..81a55b9 100644 --- a/scatterplot.cpp +++ b/scatterplot.cpp @@ -1,5 +1,6 @@ #include "scatterplot.h" +#include "geometry.h" #include <cmath> static const qreal GLYPH_OPACITY = 0.4; @@ -8,12 +9,13 @@ static const qreal GLYPH_OPACITY_SELECTED = 1.0; static const QColor OUTLINE_COLOR(0, 0, 0); static const QColor SELECTION_COLOR(128, 128, 128, 96); -static const int GLYPH_SIZE = 8; -static const float PADDING = 10; -static const float PI = 3.1415f; +static const int GLYPH_SIZE = 8.f; +static const float PADDING = 10.f; Scatterplot::Scatterplot(QQuickItem *parent) : QQuickItem(parent) + , m_sx(0, 1, 0, 1) + , m_sy(0, 1, 0, 1) , m_currentInteractionState(INTERACTION_NONE) , m_shouldUpdateGeometry(false) , m_shouldUpdateMaterials(false) @@ -59,10 +61,8 @@ void Scatterplot::setXY(const arma::mat &xy) } m_xy = xy; - m_xmin = xy.col(0).min(); - m_xmax = xy.col(0).max(); - m_ymin = xy.col(1).min(); - m_ymax = xy.col(1).max(); + m_sx.setDomain(m_xy.col(0).min(), m_xy.col(0).max()); + m_sy.setDomain(m_xy.col(1).min(), m_xy.col(1).max()); updateGeometry(); @@ -93,51 +93,19 @@ void Scatterplot::updateMaterials() update(); } -static int calculateCircleVertexCount(qreal radius) -{ - // 10 * sqrt(r) \approx 2*pi / acos(1 - 1 / (4*r)) - return (int) (10.0 * sqrt(radius)); -} - -void updateCircleGeometry(QSGGeometry *geometry, float size, float cx, float cy) +QSGNode *Scatterplot::createGlyphNodeTree() { - int vertexCount = geometry->vertexCount(); - - float theta = 2 * PI / float(vertexCount); - float c = cosf(theta); - float s = sinf(theta); - float x = size / 2; - float y = 0; - - QSGGeometry::Point2D *vertexData = geometry->vertexDataAsPoint2D(); - for (int i = 0; i < vertexCount; i++) { - vertexData[i].set(x + cx, y + cy); - - float t = x; - x = c*x - s*y; - y = s*t + c*y; + if (m_xy.n_rows < 1) { + return 0; } -} -inline float Scatterplot::fromDataXToScreenX(float x) const -{ - return PADDING + (x - m_xmin) / (m_xmax - m_xmin) * (width() - 2*PADDING); -} - -inline float Scatterplot::fromDataYToScreenY(float y) const -{ - return PADDING + (1 - (y - m_ymin) / (m_ymax - m_ymin)) * (height() - 2*PADDING); -} - -QSGNode *Scatterplot::createGlyphNodeTree() -{ QSGNode *node = new QSGNode; int vertexCount = calculateCircleVertexCount(GLYPH_SIZE / 2); for (arma::uword i = 0; i < m_xy.n_rows; i++) { QSGGeometry *glyphOutlineGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); glyphOutlineGeometry->setDrawingMode(GL_LINE_LOOP); - updateCircleGeometry(glyphOutlineGeometry, GLYPH_SIZE, 0, 0); + updateCircleGeometry(glyphOutlineGeometry, GLYPH_SIZE / 2, 0, 0); QSGGeometryNode *glyphOutlineNode = new QSGGeometryNode; glyphOutlineNode->setGeometry(glyphOutlineGeometry); glyphOutlineNode->setFlag(QSGNode::OwnsGeometry); @@ -149,7 +117,7 @@ QSGNode *Scatterplot::createGlyphNodeTree() QSGGeometry *glyphGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); glyphGeometry->setDrawingMode(GL_POLYGON); - updateCircleGeometry(glyphGeometry, GLYPH_SIZE - 1, 0, 0); + updateCircleGeometry(glyphGeometry, GLYPH_SIZE / 2 - 0.5, 0, 0); QSGGeometryNode *glyphNode = new QSGGeometryNode; glyphNode->setGeometry(glyphGeometry); glyphNode->setFlag(QSGNode::OwnsGeometry); @@ -171,20 +139,23 @@ QSGNode *Scatterplot::createGlyphNodeTree() QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { - if (m_xy.n_rows < 1) { - return 0; - } - - qreal x, y, tx, ty, moveTranslationF; - QSGNode *root = 0; if (!oldNode) { root = new QSGNode; - root->appendChildNode(createGlyphNodeTree()); + QSGNode *glyphTreeRoot = createGlyphNodeTree(); + if (glyphTreeRoot) { + root->appendChildNode(glyphTreeRoot); + } } else { root = oldNode; } + if (m_xy.n_rows < 1) { + return root; + } + + qreal x, y, tx, ty, moveTranslationF; + if (m_currentInteractionState == INTERACTION_MOVING) { tx = m_dragCurrentPos.x() - m_dragOriginPos.x(); ty = m_dragCurrentPos.y() - m_dragOriginPos.y(); @@ -192,6 +163,9 @@ QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) tx = ty = 0; } + m_sx.setRange(PADDING, width() - 2*PADDING); + m_sy.setRange(height() - 2*PADDING, PADDING); + QSGNode *node = root->firstChild()->firstChild(); for (arma::uword i = 0; i < m_xy.n_rows; i++) { arma::rowvec row = m_xy.row(i); @@ -204,15 +178,15 @@ QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) QSGGeometryNode *glyphNode = static_cast<QSGGeometryNode *>(node->firstChild()->nextSibling()); if (m_shouldUpdateGeometry) { moveTranslationF = isSelected ? 1.0 : 0.0; - x = fromDataXToScreenX(row[0]) + tx * moveTranslationF; - y = fromDataYToScreenY(row[1]) + ty * moveTranslationF; + x = m_sx(row[0]) + tx * moveTranslationF; + y = m_sy(row[1]) + ty * moveTranslationF; QSGGeometry *geometry = glyphOutlineNode->geometry(); - updateCircleGeometry(geometry, GLYPH_SIZE, x, y); + updateCircleGeometry(geometry, GLYPH_SIZE / 2, x, y); glyphOutlineNode->markDirty(QSGNode::DirtyGeometry); geometry = glyphNode->geometry(); - updateCircleGeometry(geometry, GLYPH_SIZE - 1, x, y); + updateCircleGeometry(geometry, GLYPH_SIZE / 2 - 0.5, x, y); glyphNode->markDirty(QSGNode::DirtyGeometry); } if (m_shouldUpdateMaterials) { @@ -263,7 +237,7 @@ void Scatterplot::mousePressEvent(QMouseEvent *event) case INTERACTION_NONE: case INTERACTION_SELECTED: if (event->modifiers() == Qt::AltModifier) { - m_currentInteractionState = INTERACTION_MOVING; + m_currentInteractionState = INTERACTION_BEGIN_MOVING; } else { m_currentInteractionState = INTERACTION_SELECTING; } @@ -271,6 +245,7 @@ void Scatterplot::mousePressEvent(QMouseEvent *event) m_dragCurrentPos = m_dragOriginPos; break; case INTERACTION_SELECTING: + case INTERACTION_BEGIN_MOVING: case INTERACTION_MOVING: event->ignore(); return; @@ -287,11 +262,11 @@ void Scatterplot::mouseMoveEvent(QMouseEvent *event) m_dragCurrentPos = event->localPos(); update(); break; + case INTERACTION_BEGIN_MOVING: + m_currentInteractionState = INTERACTION_MOVING; case INTERACTION_MOVING: m_dragCurrentPos = event->localPos(); - applyManipulation(); updateGeometry(); - m_dragOriginPos = m_dragCurrentPos; break; case INTERACTION_SELECTED: event->ignore(); @@ -310,11 +285,14 @@ void Scatterplot::mouseReleaseEvent(QMouseEvent *event) : INTERACTION_NONE; } break; - + case INTERACTION_BEGIN_MOVING: + m_currentInteractionState = INTERACTION_SELECTED; + break; case INTERACTION_MOVING: m_currentInteractionState = INTERACTION_SELECTED; + applyManipulation(); updateGeometry(); - emit xyChanged(m_xy); + m_dragOriginPos = m_dragCurrentPos; break; case INTERACTION_NONE: case INTERACTION_SELECTED: @@ -329,15 +307,21 @@ bool Scatterplot::updateSelection(bool mergeSelection) selection.unite(m_selectedGlyphs); } - qreal originX = m_dragOriginPos.x() / width() * (m_xmax - m_xmin) + m_xmin; - qreal originY = (1 - m_dragOriginPos.y() / height()) * (m_ymax - m_ymin) + m_ymin; - qreal currentX = m_dragCurrentPos.x() / width() * (m_xmax - m_xmin) + m_xmin; - qreal currentY = (1 - m_dragCurrentPos.y() / height()) * (m_ymax - m_ymin) + m_ymin; + m_sx.reverse(); + m_sy.reverse(); + + float originX = m_sx(m_dragOriginPos.x()); + float originY = m_sy(m_dragOriginPos.y()); + float currentX = m_sx(m_dragCurrentPos.x()); + float currentY = m_sy(m_dragCurrentPos.y()); + + m_sy.reverse(); + m_sx.reverse(); QRectF selectionRect(QPointF(originX, originY), QPointF(currentX, currentY)); for (arma::uword i = 0; i < m_xy.n_rows; i++) { - arma::rowvec row = m_xy.row(i); + const arma::rowvec &row = m_xy.row(i); if (selectionRect.contains(row[0], row[1])) { selection.insert(i); @@ -358,19 +342,22 @@ void Scatterplot::setSelection(const QSet<int> &selection) void Scatterplot::applyManipulation() { + m_sx.reverse(); + m_sy.reverse(); + LinearScale rx = m_sx; + LinearScale ry = m_sy; + m_sy.reverse(); + m_sx.reverse(); + float tx = m_dragCurrentPos.x() - m_dragOriginPos.x(); float ty = m_dragCurrentPos.y() - m_dragOriginPos.y(); - tx /= (width() - PADDING); - ty /= -(height() - PADDING); - - float x_extent = m_xmax - m_xmin; - float y_extent = m_ymax - m_ymin; - for (auto it = m_selectedGlyphs.cbegin(); it != m_selectedGlyphs.cend(); it++) { arma::rowvec row = m_xy.row(*it); - row[0] = ((row[0] - m_xmin) / x_extent + tx) * x_extent + m_xmin; - row[1] = ((row[1] - m_ymin) / y_extent + ty) * y_extent + m_ymin; + row[0] = rx(m_sx(row[0]) + tx); + row[1] = ry(m_sy(row[1]) + ty); m_xy.row(*it) = row; } + + emit xyChanged(m_xy); } diff --git a/scatterplot.h b/scatterplot.h index 3d7fa36..43bfc57 100644 --- a/scatterplot.h +++ b/scatterplot.h @@ -6,6 +6,7 @@ #include <QtQuick> #include "colorscale.h" +#include "scale.h" class Scatterplot : public QQuickItem { @@ -37,21 +38,19 @@ private: QSGNode *createGlyphNodeTree(); bool updateSelection(bool mergeSelection); - float fromDataXToScreenX(float x) const; - float fromDataYToScreenY(float y) const; - void applyManipulation(); void updateGeometry(); void updateMaterials(); arma::mat m_xy; - float m_xmin, m_xmax, m_ymin, m_ymax; + LinearScale m_sx, m_sy; enum InteractionState { INTERACTION_NONE, INTERACTION_SELECTING, INTERACTION_SELECTED, + INTERACTION_BEGIN_MOVING, INTERACTION_MOVING } m_currentInteractionState; |