aboutsummaryrefslogtreecommitdiff
path: root/scatterplot.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'scatterplot.cpp')
-rw-r--r--scatterplot.cpp343
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);
}