diff options
Diffstat (limited to 'scatterplot.cpp')
-rw-r--r-- | scatterplot.cpp | 343 |
1 files changed, 215 insertions, 128 deletions
diff --git a/scatterplot.cpp b/scatterplot.cpp index 0c2fbc1..e22eafd 100644 --- a/scatterplot.cpp +++ b/scatterplot.cpp @@ -1,108 +1,135 @@ #include "scatterplot.h" +#include "geometry.h" #include <cmath> -static const qreal GLYPH_OPACITY = 0.3; +static const qreal GLYPH_OPACITY = 0.4; static const qreal GLYPH_OPACITY_SELECTED = 1.0; -static const QColor SELECTION_COLOR(QColor(128, 128, 128, 96)); -static const int GLYPH_SIZE = 8; -static const float PADDING = 10; -static const float PI = 3.1415f; + +static const QColor OUTLINE_COLOR(0, 0, 0); +static const QColor SELECTION_COLOR(128, 128, 128, 96); + +static const int GLYPH_SIZE = 8.f; +static const float PADDING = 10.f; Scatterplot::Scatterplot(QQuickItem *parent) : QQuickItem(parent) - , m_colorScale{ - QColor("#1f77b4"), - QColor("#ff7f0e"), - QColor("#2ca02c"), - QColor("#d62728"), - QColor("#9467bd"), - QColor("#8c564b"), - QColor("#e377c2"), - QColor("#17becf"), - QColor("#7f7f7f"), - } + , m_sx(0, 1, 0, 1) + , m_sy(0, 1, 0, 1) + , m_currentInteractionState(INTERACTION_NONE) + , m_shouldUpdateGeometry(false) + , m_shouldUpdateMaterials(false) { setClip(true); setFlag(QQuickItem::ItemHasContents); } -Scatterplot::~Scatterplot() -{ -} - -void Scatterplot::setData(const arma::mat &data) +void Scatterplot::setColorScale(ColorScale *colorScale) { - if (data.n_cols != 3) + if (!colorScale) { return; + } - m_data = data; - m_xmin = data.col(0).min(); - m_xmax = data.col(0).max(); - m_ymin = data.col(1).min(); - m_ymax = data.col(1).max(); - - m_colorScale.setExtents(m_data.col(2).min(), m_data.col(2).max()); - m_selectedGlyphs.clear(); + m_colorScale = colorScale; + if (m_colorData.n_elem > 0) { + updateMaterials(); + } +} - update(); +arma::mat Scatterplot::XY() const +{ + return m_xy; } -int calculateCircleVertexCount(qreal radius) +bool Scatterplot::saveToFile(const QUrl &url) { - // 10 * sqrt(r) \approx 2*pi / acos(1 - 1 / (4*r)) - return (int) (10.0 * sqrt(radius)); + if (!url.isLocalFile()) { + return false; + } + + return m_xy.save(url.path().toStdString(), arma::raw_ascii); } -void updateCircleGeometry(QSGGeometry *geometry, float size, float cx, float cy) +void Scatterplot::setXY(const arma::mat &xy) { - int vertexCount = geometry->vertexCount(); + if (xy.n_cols != 2) { + return; + } - float theta = 2 * PI / float(vertexCount); - float c = cosf(theta); - float s = sinf(theta); - float x = size / 2; - float y = 0; + if (m_xy.n_elem != xy.n_elem) { + m_selectedGlyphs.clear(); + } - QSGGeometry::Point2D *vertexData = geometry->vertexDataAsPoint2D(); - for (int i = 0; i < vertexCount; i++) { - vertexData[i].set(x + cx, y + cy); + m_xy = xy; + 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()); - float t = x; - x = c*x - s*y; - y = s*t + c*y; + updateGeometry(); + + emit xyChanged(m_xy); +} + +void Scatterplot::setColorData(const arma::vec &colorData) +{ + if (colorData.n_elem != m_xy.n_rows) { + return; } + + m_colorData = colorData; + emit colorDataChanged(m_colorData); + + updateMaterials(); } -inline float Scatterplot::fromDataXToScreenX(float x) +void Scatterplot::updateGeometry() { - return PADDING + (x - m_xmin) / (m_xmax - m_xmin) * (width() - 2*PADDING); + m_shouldUpdateGeometry = true; + update(); } -inline float Scatterplot::fromDataYToScreenY(float y) +void Scatterplot::updateMaterials() { - return PADDING + (y - m_ymin) / (m_ymax - m_ymin) * (height() - 2*PADDING); + m_shouldUpdateMaterials = true; + update(); } -QSGNode *Scatterplot::newGlyphNodeTree() { +QSGNode *Scatterplot::createGlyphNodeTree() +{ + if (m_xy.n_rows < 1) { + return 0; + } + QSGNode *node = new QSGNode; int vertexCount = calculateCircleVertexCount(GLYPH_SIZE / 2); - for (arma::uword i = 0; i < m_data.n_rows; i++) { - QSGGeometryNode *glyphNode = new QSGGeometryNode; + 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 / 2, 0, 0); + QSGGeometryNode *glyphOutlineNode = new QSGGeometryNode; + glyphOutlineNode->setGeometry(glyphOutlineGeometry); + glyphOutlineNode->setFlag(QSGNode::OwnsGeometry); - QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); - geometry->setDrawingMode(GL_POLYGON); - glyphNode->setGeometry(geometry); + QSGFlatColorMaterial *material = new QSGFlatColorMaterial; + material->setColor(OUTLINE_COLOR); + glyphOutlineNode->setMaterial(material); + glyphOutlineNode->setFlag(QSGNode::OwnsMaterial); + + QSGGeometry *glyphGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); + glyphGeometry->setDrawingMode(GL_POLYGON); + updateCircleGeometry(glyphGeometry, GLYPH_SIZE / 2 - 0.5, 0, 0); + QSGGeometryNode *glyphNode = new QSGGeometryNode; + glyphNode->setGeometry(glyphGeometry); glyphNode->setFlag(QSGNode::OwnsGeometry); - QSGFlatColorMaterial *material = new QSGFlatColorMaterial; - material->setColor(m_colorScale.color(m_data(i, 2))); + 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); } @@ -112,47 +139,75 @@ QSGNode *Scatterplot::newGlyphNodeTree() { QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { - if (m_data.n_rows < 1) - return 0; - - qreal x, y, xt, yt, moveTranslationF; - QSGNode *root = 0; if (!oldNode) { root = new QSGNode; - root->appendChildNode(newGlyphNodeTree()); + QSGNode *glyphTreeRoot = createGlyphNodeTree(); + if (glyphTreeRoot) { + root->appendChildNode(glyphTreeRoot); + } } else { root = oldNode; } - if (m_currentState != INTERACTION_MOVING) - xt = yt = 0; - else { - xt = m_dragCurrentPos.x() - m_dragOriginPos.x(); - yt = m_dragCurrentPos.y() - m_dragOriginPos.y(); + 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(); + } else { + 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_data.n_rows; i++) { - arma::rowvec row = m_data.row(i); + for (arma::uword i = 0; i < m_xy.n_rows; i++) { + arma::rowvec row = m_xy.row(i); bool isSelected = m_selectedGlyphs.contains(i); QSGOpacityNode *glyphOpacityNode = static_cast<QSGOpacityNode *>(node); glyphOpacityNode->setOpacity(isSelected ? GLYPH_OPACITY_SELECTED : GLYPH_OPACITY); - QSGGeometryNode *glyphNode = static_cast<QSGGeometryNode *>(node->firstChild()); - QSGGeometry *geometry = glyphNode->geometry(); - moveTranslationF = isSelected ? 1.0 : 0.0; - x = fromDataXToScreenX(row[0]) + xt * moveTranslationF; - y = fromDataYToScreenY(row[1]) + yt * moveTranslationF; - updateCircleGeometry(geometry, GLYPH_SIZE, x, y); - glyphNode->markDirty(QSGNode::DirtyGeometry); + QSGGeometryNode *glyphOutlineNode = static_cast<QSGGeometryNode *>(node->firstChild()); + QSGGeometryNode *glyphNode = static_cast<QSGGeometryNode *>(node->firstChild()->nextSibling()); + if (m_shouldUpdateGeometry) { + moveTranslationF = isSelected ? 1.0 : 0.0; + x = m_sx(row[0]) + tx * moveTranslationF; + y = m_sy(row[1]) + ty * moveTranslationF; + + QSGGeometry *geometry = glyphOutlineNode->geometry(); + updateCircleGeometry(geometry, GLYPH_SIZE / 2, x, y); + glyphOutlineNode->markDirty(QSGNode::DirtyGeometry); + + geometry = glyphNode->geometry(); + updateCircleGeometry(geometry, GLYPH_SIZE / 2 - 0.5, x, y); + glyphNode->markDirty(QSGNode::DirtyGeometry); + } + if (m_shouldUpdateMaterials) { + QSGFlatColorMaterial *material = static_cast<QSGFlatColorMaterial *>(glyphNode->material()); + material->setColor(m_colorScale->color(m_colorData[i])); + glyphNode->setMaterial(material); + glyphNode->markDirty(QSGNode::DirtyMaterial); + } node = node->nextSibling(); } - // Draw selection - if (m_currentState == INTERACTION_SELECTING) { + if (m_shouldUpdateGeometry) { + m_shouldUpdateGeometry = false; + } + if (m_shouldUpdateMaterials) { + m_shouldUpdateMaterials = false; + } + + // Selection rect + if (m_currentInteractionState == INTERACTION_SELECTING) { QSGSimpleRectNode *selectionNode = 0; if (!root->firstChild()->nextSibling()) { selectionNode = new QSGSimpleRectNode; @@ -165,9 +220,11 @@ QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) selectionNode->setRect(QRectF(m_dragOriginPos, m_dragCurrentPos)); selectionNode->markDirty(QSGNode::DirtyGeometry); } else { - if (root->firstChild()->nextSibling()) { - root->firstChild()->nextSibling()->markDirty(QSGNode::DirtyGeometry); - root->removeChildNode(root->firstChild()->nextSibling()); + node = root->firstChild()->nextSibling(); + if (node) { + // node->markDirty(QSGNode::DirtyGeometry); + root->removeChildNode(node); + delete node; } } @@ -176,15 +233,19 @@ QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) void Scatterplot::mousePressEvent(QMouseEvent *event) { - switch (m_currentState) { + switch (m_currentInteractionState) { case INTERACTION_NONE: case INTERACTION_SELECTED: - m_currentState = (event->button() == Qt::MiddleButton) ? INTERACTION_MOVING - : INTERACTION_SELECTING; + if (event->modifiers() == Qt::AltModifier) { + m_currentInteractionState = INTERACTION_BEGIN_MOVING; + } else { + m_currentInteractionState = INTERACTION_SELECTING; + } m_dragOriginPos = event->localPos(); m_dragCurrentPos = m_dragOriginPos; break; case INTERACTION_SELECTING: + case INTERACTION_BEGIN_MOVING: case INTERACTION_MOVING: event->ignore(); return; @@ -193,18 +254,20 @@ void Scatterplot::mousePressEvent(QMouseEvent *event) void Scatterplot::mouseMoveEvent(QMouseEvent *event) { - switch (m_currentState) { + switch (m_currentInteractionState) { + case INTERACTION_NONE: + // event->localPos() + break; case INTERACTION_SELECTING: m_dragCurrentPos = event->localPos(); update(); break; + case INTERACTION_BEGIN_MOVING: + m_currentInteractionState = INTERACTION_MOVING; case INTERACTION_MOVING: m_dragCurrentPos = event->localPos(); - updateData(); - update(); - m_dragOriginPos = m_dragCurrentPos; + updateGeometry(); break; - case INTERACTION_NONE: case INTERACTION_SELECTED: event->ignore(); return; @@ -213,19 +276,23 @@ void Scatterplot::mouseMoveEvent(QMouseEvent *event) void Scatterplot::mouseReleaseEvent(QMouseEvent *event) { - bool mergeSelection; - - switch (m_currentState) { + switch (m_currentInteractionState) { case INTERACTION_SELECTING: - mergeSelection = (event->button() == Qt::RightButton); - m_currentState = selectGlyphs(mergeSelection) ? INTERACTION_SELECTED - : INTERACTION_NONE; - update(); + { + bool mergeSelection = (event->modifiers() == Qt::ControlModifier); + m_currentInteractionState = + updateSelection(mergeSelection) ? INTERACTION_SELECTED + : INTERACTION_NONE; + } + break; + case INTERACTION_BEGIN_MOVING: + m_currentInteractionState = INTERACTION_SELECTED; break; - case INTERACTION_MOVING: - m_currentState = INTERACTION_SELECTED; - update(); + m_currentInteractionState = INTERACTION_SELECTED; + applyManipulation(); + updateGeometry(); + m_dragOriginPos = m_dragCurrentPos; break; case INTERACTION_NONE: case INTERACTION_SELECTED: @@ -233,44 +300,64 @@ void Scatterplot::mouseReleaseEvent(QMouseEvent *event) } } -bool Scatterplot::selectGlyphs(bool mergeSelection) +bool Scatterplot::updateSelection(bool mergeSelection) { - if (!mergeSelection) - m_selectedGlyphs.clear(); + QSet<int> selection; + if (mergeSelection) { + selection.unite(m_selectedGlyphs); + } - qreal x, y; + m_sx.reverse(); + m_sy.reverse(); - 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 = fromDataXToScreenX(row[0]); - y = fromDataYToScreenY(row[1]); + 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()); - if (selectionRect.contains(x, y)) { - m_selectedGlyphs.insert(i); - if (!anySelected) - anySelected = true; + 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++) { + const arma::rowvec &row = m_xy.row(i); + + if (selectionRect.contains(row[0], row[1])) { + selection.insert(i); } } - return anySelected; + setSelection(selection); + return !selection.isEmpty(); } -void Scatterplot::updateData() +void Scatterplot::setSelection(const QSet<int> &selection) { - float xt = m_dragCurrentPos.x() - m_dragOriginPos.x(); - float yt = m_dragCurrentPos.y() - m_dragOriginPos.y(); + m_selectedGlyphs = selection; + update(); + + emit selectionChanged(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(); - xt /= (width() - PADDING); - yt /= (height() - PADDING); for (auto it = m_selectedGlyphs.cbegin(); it != m_selectedGlyphs.cend(); it++) { - arma::rowvec row = m_data.row(*it); - row[0] = ((row[0] - m_xmin) / (m_xmax - m_xmin) + xt) * (m_xmax - m_xmin) + m_xmin; - row[1] = ((row[1] - m_ymin) / (m_ymax - m_ymin) + yt) * (m_ymax - m_ymin) + m_ymin; - m_data.row(*it) = row; + arma::rowvec row = m_xy.row(*it); + row[0] = rx(m_sx(row[0]) + tx); + row[1] = ry(m_sy(row[1]) + ty); + m_xy.row(*it) = row; } - // does not send last column (labels) - emit dataChanged(m_data.cols(0, m_data.n_cols - 2)); + emit xyInteractivelyChanged(m_xy); } |