From b3c7ac156e1c4ac5d7455ce61e89549291ac85b1 Mon Sep 17 00:00:00 2001 From: Samuel Fadel Date: Wed, 20 May 2015 18:53:07 -0300 Subject: Nearly complete implementation of interaction. Interaction still does not work due to signals not being correctly emitted. --- interactionhandler.cpp | 31 ++++++++++++++ interactionhandler.h | 33 +++++++++++++++ main.cpp | 33 +++++++-------- main_view.qml | 4 +- pm.pro | 2 + scatterplot.cpp | 111 +++++++++++++++++++++++++++++++++++++++---------- scatterplot.h | 9 ++-- 7 files changed, 180 insertions(+), 43 deletions(-) create mode 100644 interactionhandler.cpp create mode 100644 interactionhandler.h diff --git a/interactionhandler.cpp b/interactionhandler.cpp new file mode 100644 index 0000000..fab3c75 --- /dev/null +++ b/interactionhandler.cpp @@ -0,0 +1,31 @@ +#include "interactionhandler.h" + +#include "mp.h" + +InteractionHandler::InteractionHandler(const arma::mat &X, + const arma::vec &labels, + const arma::uvec &sampleIndices) + : m_X(X) + , m_labels(labels) + , m_sampleIndices(sampleIndices) + , m_technique(TECHNIQUE_LAMP) +{ +} + +void InteractionHandler::setSubsample(const arma::mat &Ys) +{ + arma::mat embedding(m_X.n_rows, Ys.n_cols); + switch (m_technique) { + case TECHNIQUE_PLMP: + case TECHNIQUE_LSP: + case TECHNIQUE_LAMP: + embedding = mp::lamp(m_X, m_sampleIndices, Ys); + break; + } + + arma::mat Y(embedding.n_rows, embedding.n_cols); + Y.cols(0, embedding.n_cols - 1) = embedding; + Y.col(Y.n_cols - 1) = m_labels; + + emit subsampleChanged(Y); +} diff --git a/interactionhandler.h b/interactionhandler.h new file mode 100644 index 0000000..3dbeb8a --- /dev/null +++ b/interactionhandler.h @@ -0,0 +1,33 @@ +#ifndef INTERACTIONHANDLER_H +#define INTERACTIONHANDLER_H + +#include + +#include "scatterplot.h" + +class InteractionHandler : public QObject +{ + Q_OBJECT +public: + enum InteractiveTechnique { + TECHNIQUE_PLMP, + TECHNIQUE_LAMP, + TECHNIQUE_LSP + }; + + InteractionHandler(const arma::mat &X, const arma::vec &labels, const arma::uvec &sampleIndices); + +signals: + void subsampleChanged(const arma::mat &Y); + +public slots: + void setSubsample(const arma::mat &Ys); + +private: + arma::mat m_X; + arma::vec m_labels; + arma::uvec m_sampleIndices; + InteractiveTechnique m_technique; +}; + +#endif // INTERACTIONHANDLER_H diff --git a/main.cpp b/main.cpp index 4fcff65..3f2a85e 100644 --- a/main.cpp +++ b/main.cpp @@ -1,10 +1,12 @@ #include +#include #include #include #include #include "mp.h" #include "scatterplot.h" +#include "interactionhandler.h" int main(int argc, char **argv) { @@ -12,17 +14,12 @@ int main(int argc, char **argv) qmlRegisterType("PM", 1, 0, "Scatterplot"); - /*QQuickView view; - QSurfaceFormat format = view.format(); - format.setSamples(16); - view.setFormat(format); - view.setResizeMode(QQuickView::SizeRootObjectToView); - view.setSource(QUrl("qrc:///main_view.qml"));*/ QSurfaceFormat fmt; fmt.setSamples(16); QSurfaceFormat::setDefaultFormat(fmt); QQmlApplicationEngine engine(QUrl("qrc:///main_view.qml")); + std::unique_ptr interactionHandler((InteractionHandler *) 0); if (argc > 1) { arma::mat dataset; dataset.load(argv[1], arma::raw_ascii); @@ -34,20 +31,22 @@ int main(int argc, char **argv) arma::uvec sampleIndices = arma::randi(subsampleSize, arma::distr_param(0, n-1)); arma::mat Ys = arma::randn(subsampleSize, 2); Ys = mp::forceScheme(mp::dist(X.rows(sampleIndices)), Ys); - - // Plot the subsample - Scatterplot *plot = engine.rootObjects()[0]->findChild("subsamplePlot"); arma::mat subsampleData(subsampleSize, 3); subsampleData.cols(0, 1) = Ys; subsampleData.col(2) = labels(sampleIndices); - plot->setData(subsampleData); - - // Plot entire dataset - plot = engine.rootObjects()[0]->findChild("plot"); - arma::mat reducedData(n, 3); - reducedData.cols(0, 1) = mp::lamp(X, sampleIndices, Ys); - reducedData.col(2) = labels; - plot->setData(reducedData); + + Scatterplot *subsamplePlot = engine.rootObjects()[0]->findChild("subsamplePlot"); + subsamplePlot->setAcceptedMouseButtons(Qt::LeftButton | Qt::MiddleButton | Qt::RightButton); + subsamplePlot->setData(subsampleData); + Scatterplot *plot = engine.rootObjects()[0]->findChild("plot"); + + // connect both plots through interaction handler + interactionHandler = std::unique_ptr(new InteractionHandler(X, labels, sampleIndices)); + QObject::connect(subsamplePlot, SIGNAL(dataChanged(const arma::mat &)), + interactionHandler.get(), SLOT(setSubsample(const arma::mat &))); + QObject::connect(interactionHandler.get(), SIGNAL(subsampleChanged(const arma::mat &)), + plot, SLOT(setData(const arma::mat &))); + } return app.exec(); diff --git a/main_view.qml b/main_view.qml index 8389461..4a093f2 100644 --- a/main_view.qml +++ b/main_view.qml @@ -4,8 +4,8 @@ import PM 1.0 ApplicationWindow { visible: true - width: 960 - height: 480 + width: 1200 + height: 600 menuBar: MenuBar { Menu { diff --git a/pm.pro b/pm.pro index 1c6e26a..0d0f153 100644 --- a/pm.pro +++ b/pm.pro @@ -4,10 +4,12 @@ QMAKE_CXXFLAGS += -std=c++11 QMAKE_LIBS += -larmadillo HEADERS += colorscale.h \ scatterplot.h \ + interactionhandler.h \ mp.h SOURCES += main.cpp \ colorscale.cpp \ scatterplot.cpp \ + interactionhandler.cpp \ lamp.cpp \ forceScheme.cpp \ dist.cpp diff --git a/scatterplot.cpp b/scatterplot.cpp index 414f46a..dd8d378 100644 --- a/scatterplot.cpp +++ b/scatterplot.cpp @@ -1,16 +1,13 @@ #include "scatterplot.h" -#include - #include -#include #include #include #include #include #include -const int GLYPH_SIZE = 5; +const int GLYPH_SIZE = 8; const float PADDING = 10; const float PI = 3.1415f; @@ -31,7 +28,6 @@ Scatterplot::Scatterplot(QQuickItem *parent) { setClip(true); setFlag(QQuickItem::ItemHasContents); - setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton); } Scatterplot::~Scatterplot() @@ -44,7 +40,12 @@ void Scatterplot::setData(const arma::mat &data) return; m_data = data; + m_colorScale.setExtents(m_data.col(2).min(), m_data.col(2).max()); + m_selectedGlyphs.clear(); + for (arma::uword i = 0; i < m_data.n_rows; i++) + m_selectedGlyphs.append(false); + update(); } @@ -101,7 +102,7 @@ QSGNode *Scatterplot::newGlyphNodeTree() { QSGGeometryNode *glyphNode = new QSGGeometryNode; QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); - geometry->setDrawingMode(GL_POLYGON); + geometry->setDrawingMode(GL_LINE_LOOP); glyphNode->setGeometry(geometry); glyphNode->setFlag(QSGNode::OwnsGeometry); @@ -125,7 +126,7 @@ QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) xmax = m_data.col(0).max(), ymin = m_data.col(1).min(), ymax = m_data.col(1).max(), - x, y; + x, y, xt, yt, selected; QSGNode *root = 0; if (!oldNode) { @@ -136,12 +137,20 @@ QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) } QSGNode *glyphNode = root->firstChild()->firstChild(); + if (m_currentState != INTERACTION_MOVING) + xt = yt = 0; + else { + xt = m_dragCurrentPos.x() - m_dragOriginPos.x(); + yt = m_dragCurrentPos.y() - m_dragOriginPos.y(); + } for (arma::uword i = 0; i < m_data.n_rows; i++) { arma::rowvec row = m_data.row(i); - x = PADDING + (row[0] - xmin) / (xmax - xmin) * (width() - 2*PADDING); - y = PADDING + (row[1] - ymin) / (ymax - ymin) * (height() - 2*PADDING); + selected = m_selectedGlyphs[i] ? 1.0 : 0.0; + x = PADDING + (row[0] - xmin) / (xmax - xmin) * (width() - 2*PADDING) + xt * selected; + y = PADDING + (row[1] - ymin) / (ymax - ymin) * (height() - 2*PADDING) + yt * selected; QSGGeometry *geometry = static_cast(glyphNode)->geometry(); + geometry->setDrawingMode(m_selectedGlyphs[i] ? GL_POLYGON : GL_LINE_LOOP); updateCircleGeometry(geometry, GLYPH_SIZE, x, y); glyphNode->markDirty(QSGNode::DirtyGeometry); glyphNode = glyphNode->nextSibling(); @@ -153,12 +162,12 @@ QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) if (!root->firstChild()->nextSibling()) { selectionNode = new QSGGeometryNode; QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4); - geometry->setDrawingMode(GL_POLYGON); + geometry->setDrawingMode(GL_LINE_LOOP); selectionNode->setGeometry(geometry); selectionNode->setFlag(QSGNode::OwnsGeometry); QSGFlatColorMaterial *material = new QSGFlatColorMaterial; - material->setColor(QColor(128, 128, 128, 128)); + material->setColor(QColor(0, 0, 0, 128)); selectionNode->setMaterial(material); selectionNode->setFlag(QSGNode::OwnsMaterial); @@ -169,7 +178,7 @@ QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) updateSelectionGeometry(selectionNode->geometry(), m_dragOriginPos, m_dragCurrentPos); selectionNode->markDirty(QSGNode::DirtyGeometry); - } else if (m_currentState == INTERACTION_NONE) { + } else { if (root->firstChild()->nextSibling()) { root->firstChild()->nextSibling()->markDirty(QSGNode::DirtyGeometry); root->removeChildNode(root->firstChild()->nextSibling()); @@ -183,12 +192,12 @@ void Scatterplot::mousePressEvent(QMouseEvent *event) { switch (m_currentState) { case INTERACTION_NONE: - m_currentState = INTERACTION_SELECTING; + case INTERACTION_SELECTED: + m_currentState = (event->button() == Qt::MiddleButton) ? INTERACTION_MOVING + : INTERACTION_SELECTING; m_dragOriginPos = event->localPos(); + m_dragCurrentPos = m_dragOriginPos; break; - case INTERACTION_SELECTED: - m_currentState = INTERACTION_MOVING; - break; // TODO case INTERACTION_SELECTING: case INTERACTION_MOVING: return; // should not be reached @@ -197,25 +206,85 @@ void Scatterplot::mousePressEvent(QMouseEvent *event) void Scatterplot::mouseMoveEvent(QMouseEvent *event) { - if (m_currentState != INTERACTION_SELECTING) + switch (m_currentState) { + case INTERACTION_NONE: + case INTERACTION_SELECTED: return; - - m_dragCurrentPos = event->localPos(); - update(); + case INTERACTION_SELECTING: + case INTERACTION_MOVING: + m_dragCurrentPos = event->localPos(); + update(); + } } void Scatterplot::mouseReleaseEvent(QMouseEvent *event) { + bool mergeSelection; + switch (m_currentState) { case INTERACTION_SELECTING: - m_currentState = INTERACTION_NONE; + mergeSelection = (event->button() == Qt::RightButton); + m_currentState = selectGlyphs(mergeSelection) ? INTERACTION_SELECTED + : INTERACTION_NONE; update(); break; case INTERACTION_MOVING: + m_currentState = INTERACTION_SELECTED; + updateData(); + update(); break; case INTERACTION_NONE: case INTERACTION_SELECTED: return; // should not be reached } } + +bool Scatterplot::selectGlyphs(bool mergeSelection) +{ + qreal xmin = m_data.col(0).min(), + xmax = m_data.col(0).max(), + ymin = m_data.col(1).min(), + ymax = m_data.col(1).max(), + x, y; + + QRectF selectionRect(m_dragOriginPos, m_dragCurrentPos); + bool anySelected = false; + for (arma::uword i = 0; i < m_data.n_rows; i++) { + arma::rowvec row = m_data.row(i); + x = PADDING + (row[0] - xmin) / (xmax - xmin) * (width() - 2*PADDING); + y = PADDING + (row[1] - ymin) / (ymax - ymin) * (height() - 2*PADDING); + + bool contains = selectionRect.contains(x, y); + anySelected = anySelected || contains; + m_selectedGlyphs[i] = (mergeSelection && m_selectedGlyphs[i]) || contains; + } + + return anySelected; +} + +void Scatterplot::updateData() +{ + qreal xmin = m_data.col(0).min(), + xmax = m_data.col(0).max(), + ymin = m_data.col(1).min(), + ymax = m_data.col(1).max(); + + float xt = m_dragCurrentPos.x() - m_dragOriginPos.x(); + float yt = m_dragCurrentPos.y() - m_dragOriginPos.y(); + + xt /= (width() - PADDING); + yt /= (height() - PADDING); + for (arma::uword i = 0; i < m_data.n_rows; i++) { + if (!m_selectedGlyphs[i]) + continue; + + arma::rowvec row = m_data.row(i); + row[0] = ((row[0] - xmin) / (xmax - xmin) + xt) * (xmax - xmin) + xmin; + row[1] = ((row[1] - ymin) / (ymax - ymin) + yt) * (ymax - ymin) + ymin; + m_data.row(i) = row; + } + + // does not send last column (labels) + emit dataChanged(m_data.cols(0, m_data.n_cols - 2)); +} diff --git a/scatterplot.h b/scatterplot.h index 58774a9..5423a82 100644 --- a/scatterplot.h +++ b/scatterplot.h @@ -2,8 +2,8 @@ #define SCATTERPLOT_H #include -#include #include +#include #include "colorscale.h" @@ -14,11 +14,11 @@ public: Scatterplot(QQuickItem *parent = 0); ~Scatterplot(); - void setData(const arma::mat &data); - signals: + void dataChanged(const arma::mat &data); public slots: + void setData(const arma::mat &data); protected: QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *); @@ -28,6 +28,8 @@ protected: private: QSGNode *newGlyphNodeTree(); + bool selectGlyphs(bool mergeSelection); + void updateData(); enum InteractionState { INTERACTION_NONE, @@ -36,6 +38,7 @@ private: INTERACTION_MOVING } m_currentState; QPointF m_dragOriginPos, m_dragCurrentPos; + QList m_selectedGlyphs; arma::mat m_data; ColorScale m_colorScale; -- cgit v1.2.3