aboutsummaryrefslogtreecommitdiff
#include "scatterplot.h"

#include <algorithm>
#include <cmath>

#include "colormap.h"
#include "continuouscolorscale.h"

// Samples used in multisampling for rendering
static const size_t NUM_SAMPLES = 8;

// Glyphs settings
static const Color DEFAULT_GLYPH_COLOR(255, 255, 255);
static const float DEFAULT_GLYPH_SIZE = 8.0f;
static const float GLYPH_OPACITY = 1.0f;
static const float GLYPH_OPACITY_SELECTED = 1.0f;

static const float GLYPH_OUTLINE_WIDTH = 2.0f;
static const Color GLYPH_OUTLINE_COLOR(0, 0, 0);
static const Color GLYPH_OUTLINE_COLOR_SELECTED(20, 255, 225);

// Brush settings
// BRUSHING_MAX_DIST in quadtree.cpp
static const float CROSSHAIR_LENGTH = 8.0f;
static const float CROSSHAIR_THICKNESS1 = 1.0f;
static const float CROSSHAIR_THICKNESS2 = 0.5f;
static const Color CROSSHAIR_COLOR1(255, 255, 255);
static const Color CROSSHAIR_COLOR2(0, 0, 0);

// Selection settings
static const Color SELECTION_COLOR(128, 128, 128, 96);

// Mouse buttons
// static const Qt::MouseButton NORMAL_BUTTON = Qt::LeftButton;
// static const Qt::MouseButton SPECIAL_BUTTON = Qt::RightButton;

static const char *shaderVertex = R"EOF(#version 330

precision mediump float;

uniform float colormask;
uniform mat4 transform;
uniform sampler2D colormap;

layout (location = 0) in vec2 pos;
layout (location = 1) in float value;
// layout (location = 2) in float opacity;

out VS_OUT {
    vec4 color;
} vs_out;

vec3 getRGB(float value) {
    return texture(colormap, vec2(mix(0.005, 0.995, value), 0)).rgb;
}

void main()
{
    vs_out.color = vec4(mix(vec3(1.0), getRGB(value), colormask), 1.0);
    gl_Position = transform * vec4(pos.xy, 0.0, 1.0);
}
)EOF";

static const char *shaderFragment = R"EOF(#version 330
in vec4 color;

out vec4 FragColor;

void main()
{
    gl_FragDepth = gl_PrimitiveID / 3000;
    FragColor = color;
    // FragColor = vec4(1.0);
}
)EOF";

static const char *shaderGeometry = R"EOF(#version 330
layout (points) in;
layout (triangle_strip, max_vertices = 16) out;

uniform float size;

in VS_OUT {
    vec4 color;
} gs_in[];

out vec4 color;

#define PI 3.1415926538

void main() {
    int num = 8;
    float theta = 2 * PI / num;
    float c = cos(theta);
    float s = sin(theta);
    vec4 p = vec4(size, 0.0, 0.0, 0.0);
    // gs_in[0] as there is only one input vertex
    color = gs_in[0].color;
    for (int i = 0; i < num; i++) {
        gl_Position = gl_in[0].gl_Position + p;
        EmitVertex();

        gl_Position = gl_in[0].gl_Position + vec4(p.x, -p.y, 0.0, 0.0);
        EmitVertex();

        p.xy = vec2(c*p.x - s*p.y, s*p.x + c*p.y);
    }
    EndPrimitive();
}
)EOF";

static const char *shaderOutlineFragment = R"EOF(#version 330
in vec4 color;

out vec4 FragColor;

void main()
{
    gl_FragDepth = gl_PrimitiveID / 2000;
    FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
)EOF";

static const char *shaderOutlineGeometry = R"EOF(#version 330
layout (points) in;
layout (line_strip, max_vertices = 16) out;

uniform float size;

in VS_OUT {
    vec4 color;
} gs_in[];

out vec4 color;

#define PI 3.1415926538

void main() {
    int num = 15;
    float theta = 2 * PI / num;
    float c = cos(theta);
    float s = sin(theta);
    vec4 p = vec4(size, 0.0, 0.0, 0.0);
    // gs_in[0] as there is only one input vertex
    color = gs_in[0].color;
    for (int i = 0; i < num; i++) {
        gl_Position = gl_in[0].gl_Position + p;
        EmitVertex();

        p.xy = vec2(c*p.x - s*p.y, s*p.x + c*p.y);
    }
    gl_Position = gl_in[0].gl_Position + p;
    EmitVertex();
    EndPrimitive();
}
)EOF";

Scatterplot::Scatterplot()
    : m_glyphSize(DEFAULT_GLYPH_SIZE)
    , m_colorScale(0)
    , m_autoScale(false)
    , m_sx(0, 1, 0, 1)
    , m_sy(0, 1, 0, 1)
    , m_anySelected(false)
    , m_brushedItem(-1)
    , m_interactionState(StateNone)
    , m_dragEnabled(false)
    , m_redraw(false)
    , m_shouldUpdateGeometry(false)
    , m_shouldUpdateMaterials(false)
{
    glGenFramebuffers(1, &m_FBO);
    glGenFramebuffers(1, &m_outFBO);

    glGenBuffers(1, &m_pointsVBO);
    glGenBuffers(1, &m_valuesVBO);
    glGenVertexArrays(1, &m_VAO);
    glBindVertexArray(m_VAO);
    glBindBuffer(GL_ARRAY_BUFFER, m_pointsVBO);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
    // glVertexAttribDivisor(0, 1);

    glBindBuffer(GL_ARRAY_BUFFER, m_valuesVBO);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, nullptr);
    // glVertexAttribDivisor(1, 1);
    glBindVertexArray(0);

    glGenTextures(1, &m_depthTex);
    glGenTextures(1, &m_tex);
    glGenTextures(1, &m_outTex);

    m_shader = std::make_unique<Shader>(
        shaderVertex,
        shaderFragment,
        shaderGeometry);

    m_shaderOutline = std::make_unique<Shader>(
        shaderVertex,
        shaderOutlineFragment,
        shaderOutlineGeometry);

    std::fill(&m_transform[0][0], &m_transform[0][0] + 16, 0.0f);
    m_transform[3][3] = 1.0f;
}

Scatterplot::~Scatterplot()
{
    glDeleteFramebuffers(1, &m_FBO);
}

void Scatterplot::setSize(size_t width, size_t height)
{
    m_width = width;
    m_height = height;

    glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, m_tex);
    glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, NUM_SAMPLES,
                            GL_RGBA, m_width, m_height, GL_TRUE);
    // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);

    glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, m_depthTex);
    glTexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, NUM_SAMPLES,
                            GL_DEPTH_COMPONENT, m_width, m_height, GL_TRUE);
    glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);

    glBindTexture(GL_TEXTURE_2D, m_outTex);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, m_width, m_height, 0, GL_RGBA,
                 GL_FLOAT, 0);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glBindTexture(GL_TEXTURE_2D, 0);

    update();
}

void Scatterplot::setColormap(GLuint texture)
{
    m_colormapTex = texture;
    update();
}

arma::mat Scatterplot::XY() const
{
    return m_xy;
}

void Scatterplot::setXY(const arma::mat &xy)
{
    if (xy.n_cols != 2) {
        return;
    }

    m_xy = xy;
    xyChanged(m_xy);

    if (m_autoScale) {
        autoScale();
    }

    updateQuadTree();

    if (m_selection.size() != m_xy.n_rows) {
        m_selection.assign(m_xy.n_rows, false);
    }

    if (m_opacityData.n_elem != m_xy.n_rows) {
        // Reset opacity data
        m_opacityData.resize(xy.n_rows);
        m_opacityData.fill(GLYPH_OPACITY);
        opacityDataChanged(m_opacityData);
    }

    m_shouldUpdateGeometry = true;
    update();
}

void Scatterplot::setColorData(const arma::vec &colorData)
{
    if (m_xy.n_rows > 0
        && (colorData.n_elem > 0 && colorData.n_elem != m_xy.n_rows)) {
        return;
    }

    m_colorData = colorData;
    colorDataChanged(m_colorData);

    m_shouldUpdateMaterials = true;
    update();
}

void Scatterplot::setOpacityData(const arma::vec &opacityData)
{
    if (m_xy.n_rows > 0 && opacityData.n_elem != m_xy.n_rows) {
        return;
    }

    m_opacityData = opacityData;
    opacityDataChanged(m_opacityData);
    update();
}

void Scatterplot::setScale(const LinearScale<float> &sx, const LinearScale<float> &sy)
{
    m_sx = sx;
    m_sy = sy;
    scaleChanged(m_sx, m_sy);

    updateQuadTree();
    update();
}

void Scatterplot::setAutoScale(bool autoScale)
{
    m_autoScale = autoScale;
    if (autoScale) {
        this->autoScale();
    }
}

void Scatterplot::autoScale()
{
    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());
    scaleChanged(m_sx, m_sy);
}

void Scatterplot::setGlyphSize(float glyphSize)
{
    if (m_glyphSize == glyphSize || glyphSize < 2.0f) {
        return;
    }

    m_glyphSize = glyphSize;
    glyphSizeChanged(m_glyphSize);
    update();
}

void Scatterplot::update()
{
    m_redraw = true;

    if (m_shouldUpdateGeometry) {
        updateGeometry();
    }
    if (m_shouldUpdateMaterials) {
        updateMaterials();
    }
    updateTransform();
}

void Scatterplot::updateGeometry()
{
    if (m_xy.n_rows < 1 || m_xy.n_cols != 2) {
        return;
    }

    if (m_colorData.n_elem > 0 && m_colorData.n_elem != m_xy.n_rows) {
        // Old values are no longer valid, clean up
        m_colorData.fill(0.0f);
    }

    // Copy 'points' to internal data structure(s)
    m_points.resize(m_xy.n_rows);
    const double *col_x = m_xy.colptr(0);
    const double *col_y = m_xy.colptr(1);
    for (unsigned i = 0; i < m_xy.n_rows; i++) {
        m_points[i].set(col_x[i], col_y[i]);
    }

    glBindBuffer(GL_ARRAY_BUFFER, m_pointsVBO);
    glBufferData(GL_ARRAY_BUFFER, m_points.size() * sizeof(vec2),
                 m_points.data(), GL_DYNAMIC_DRAW);

    m_shouldUpdateGeometry = false;
}

void Scatterplot::updateMaterials()
{
    glBindBuffer(GL_ARRAY_BUFFER, m_valuesVBO);
    glBufferData(GL_ARRAY_BUFFER, m_colorData.n_elem * sizeof(float),
                 m_colorData.memptr(), GL_DYNAMIC_DRAW);

    m_shouldUpdateMaterials = false;
}

void Scatterplot::updateTransform()
{
    float padding = Scatterplot::PADDING;
    float offsetX = padding / m_width, offsetY = padding / m_height;
    updateTransform4x4(m_sx, m_sy, offsetX, offsetY, m_transform);
}

void Scatterplot::draw()
{
    if (!m_redraw) {
        return;
    }
    m_redraw = false;

    int originalFBO;
    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &originalFBO);

    glBindFramebuffer(GL_FRAMEBUFFER, m_FBO);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                           GL_TEXTURE_2D_MULTISAMPLE, m_tex, 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                           GL_TEXTURE_2D_MULTISAMPLE, m_depthTex, 0);

    glViewport(0, 0, m_width, m_height);
    m_shader->use();
    m_shader->setUniform("transform", m_transform);
    m_shader->setUniform("size", m_glyphSize / m_width);
    m_shader->setUniform("colormask", m_colorData.n_elem == 0 ? 0.0f : 1.0f);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_colormapTex);
    m_shader->setUniform("colormap", 0);

    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    // glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glBindVertexArray(m_VAO);
    glDrawArrays(GL_POINTS, 0, m_points.size());
    m_shader->release();

    // Swap out shaders to draw outline
    glLineWidth(GLYPH_OUTLINE_WIDTH);
    m_shaderOutline->use();
    m_shaderOutline->setUniform("transform", m_transform);
    m_shaderOutline->setUniform("size", m_glyphSize / m_width);
    m_shaderOutline->setUniform("colormap", 0);
    glDrawArrays(GL_POINTS, 0, m_points.size());
    glBindVertexArray(0);
    m_shaderOutline->release();
    glLineWidth(1.0f);
    glDisable(GL_DEPTH_TEST);

    glBindFramebuffer(GL_FRAMEBUFFER, m_outFBO);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                           GL_TEXTURE_2D, m_outTex, 0);

    glBindFramebuffer(GL_READ_FRAMEBUFFER, m_FBO);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_outFBO);
    glBlitFramebuffer(0, 0, m_width, m_height,
                      0, 0, m_width, m_height,
                      GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST);

    /*
    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (status != GL_FRAMEBUFFER_COMPLETE) {
        checkGLError("draw():");
    }
    */

    glBindFramebuffer(GL_FRAMEBUFFER, originalFBO);
}

/*
QSGNode *Scatterplot::newSceneGraph()
{
    // NOTE:
    // The hierarchy in the scene graph is as follows:
    // root [[splatNode] [glyphsRoot [glyph [...]]] [selectionNode]]
    QSGNode *root = new QSGNode;
    QSGNode *glyphTreeRoot = newGlyphTree();
    if (glyphTreeRoot) {
        root->appendChildNode(glyphTreeRoot);
    }

    QSGSimpleRectNode *selectionRectNode = new QSGSimpleRectNode;
    selectionRectNode->setColor(SELECTION_COLOR);
    root->appendChildNode(selectionRectNode);

    QSGTransformNode *brushNode = new QSGTransformNode;

    QSGGeometryNode *whiteCrossHairNode = new QSGGeometryNode;
    QSGGeometry *whiteCrossHairGeom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 12);
    whiteCrossHairGeom->setDrawingMode(GL_POLYGON);
    whiteCrossHairGeom->setVertexDataPattern(QSGGeometry::DynamicPattern);
    updateCrossHairGeometry(whiteCrossHairGeom, 0, 0, CROSSHAIR_THICKNESS1, CROSSHAIR_LENGTH);
    QSGFlatColorMaterial *whiteCrossHairMaterial = new QSGFlatColorMaterial;
    whiteCrossHairMaterial->setColor(CROSSHAIR_COLOR1);
    whiteCrossHairNode->setGeometry(whiteCrossHairGeom);
    whiteCrossHairNode->setMaterial(whiteCrossHairMaterial);
    whiteCrossHairNode->setFlags(QSGNode::OwnsGeometry | QSGNode::OwnsMaterial);
    brushNode->appendChildNode(whiteCrossHairNode);

    QSGGeometryNode *blackCrossHairNode = new QSGGeometryNode;
    QSGGeometry *blackCrossHairGeom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 12);
    blackCrossHairGeom->setDrawingMode(GL_POLYGON);
    blackCrossHairGeom->setVertexDataPattern(QSGGeometry::DynamicPattern);
    updateCrossHairGeometry(blackCrossHairGeom, 0, 0, CROSSHAIR_THICKNESS2, CROSSHAIR_LENGTH);
    QSGFlatColorMaterial *blackCrossHairMaterial = new QSGFlatColorMaterial;
    blackCrossHairMaterial->setColor(CROSSHAIR_COLOR2);
    blackCrossHairNode->setGeometry(blackCrossHairGeom);
    blackCrossHairNode->setMaterial(blackCrossHairMaterial);
    blackCrossHairNode->setFlags(QSGNode::OwnsGeometry | QSGNode::OwnsMaterial);
    brushNode->appendChildNode(blackCrossHairNode);

    root->appendChildNode(brushNode);

    return root;
}
*/

/*
QSGNode *Scatterplot::newGlyphTree()
{
    // NOTE:
    // The glyph graph is structured as:
    // root [opacityNode [outlineNode fillNode] ...]
    if (m_xy.n_rows < 1) {
        return 0;
    }

    QSGNode *node = new QSGNode;
    int vertexCount = calculateCircleVertexCount(m_glyphSize);

    for (arma::uword i = 0; i < m_xy.n_rows; i++) {
        QSGGeometry *glyphOutlineGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount);
        glyphOutlineGeometry->setDrawingMode(GL_POLYGON);
        glyphOutlineGeometry->setVertexDataPattern(QSGGeometry::DynamicPattern);
        QSGGeometryNode *glyphOutlineNode = new QSGGeometryNode;
        glyphOutlineNode->setGeometry(glyphOutlineGeometry);
        glyphOutlineNode->setFlag(QSGNode::OwnsGeometry);

        QSGFlatColorMaterial *material = new QSGFlatColorMaterial;
        material->setColor(GLYPH_OUTLINE_COLOR);
        glyphOutlineNode->setMaterial(material);
        glyphOutlineNode->setFlag(QSGNode::OwnsMaterial);

        QSGGeometry *glyphGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount);
        glyphGeometry->setDrawingMode(GL_POLYGON);
        glyphGeometry->setVertexDataPattern(QSGGeometry::DynamicPattern);
        QSGGeometryNode *glyphNode = new QSGGeometryNode;
        glyphNode->setGeometry(glyphGeometry);
        glyphNode->setFlag(QSGNode::OwnsGeometry);

        material = new QSGFlatColorMaterial;
        material->setColor(DEFAULT_GLYPH_COLOR);
        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);
    }

    return node;
}
*/

/*
QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
    QSGNode *root = oldNode ? oldNode : newSceneGraph();

    if (m_xy.n_rows < 1) {
        return root;
    }

    // This keeps track of where we are in the scene when updating
    QSGNode *node = root->firstChild();

    updateGlyphs(node);
    node = node->nextSibling();

    if (m_shouldUpdateGeometry) {
        m_shouldUpdateGeometry = false;
    }
    if (m_shouldUpdateMaterials) {
        m_shouldUpdateMaterials = false;
    }

    // Selection
    QSGSimpleRectNode *selectionNode = static_cast<QSGSimpleRectNode *>(node);
    if (m_interactionState == StateSelecting) {
        selectionNode->setRect(QRectF(m_dragOriginPos, m_dragCurrentPos));
        selectionNode->markDirty(QSGNode::DirtyGeometry);
    } else {
        // Hide selection rect
        selectionNode->setRect(QRectF(-1, -1, 0, 0));
    }
    node = node->nextSibling();

    // Brushing
    updateBrush(node);
    node = node->nextSibling();

    return root;
}
*/

/*
void Scatterplot::updateGlyphs(QSGNode *glyphsNode)
{
    qreal x, y, tx, ty, moveTranslationF;

    if (!m_shouldUpdateGeometry && !m_shouldUpdateMaterials) {
        return;
    }

    if (m_interactionState == StateMoving) {
        tx = m_dragCurrentPos.x() - m_dragOriginPos.x();
        ty = m_dragCurrentPos.y() - m_dragOriginPos.y();
    } else {
        tx = ty = 0;
    }

    m_sx.setRange(PADDING, width() - PADDING);
    m_sy.setRange(height() - PADDING, PADDING);

    QSGNode *node = glyphsNode->firstChild();
    for (arma::uword i = 0; i < m_xy.n_rows; i++) {
        const arma::rowvec &row = m_xy.row(i);
        bool isSelected = m_selection[i];

        QSGOpacityNode *glyphOpacityNode = static_cast<QSGOpacityNode *>(node);
        glyphOpacityNode->setOpacity(m_opacityData[i]);

        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, m_glyphSize, x, y);
            glyphOutlineNode->markDirty(QSGNode::DirtyGeometry);

            geometry = glyphNode->geometry();
            updateCircleGeometry(geometry, m_glyphSize - 2*GLYPH_OUTLINE_WIDTH, x, y);
            glyphNode->markDirty(QSGNode::DirtyGeometry);
        }
        if (m_shouldUpdateMaterials) {
            QSGFlatColorMaterial *material = static_cast<QSGFlatColorMaterial *>(glyphOutlineNode->material());
            material->setColor(isSelected ? GLYPH_OUTLINE_COLOR_SELECTED
                                          : GLYPH_OUTLINE_COLOR);
            glyphOutlineNode->markDirty(QSGNode::DirtyMaterial);

            material = static_cast<QSGFlatColorMaterial *>(glyphNode->material());
            if (m_colorData.n_elem > 0) {
                material->setColor(m_colorScale->color(m_colorData[i]));
            } else {
                material->setColor(DEFAULT_GLYPH_COLOR);
            }
            glyphNode->markDirty(QSGNode::DirtyMaterial);
        }

        node = node->nextSibling();
    }

    // XXX: Beware: QSGNode::DirtyForceUpdate is undocumented
    //
    // This causes the scene graph to correctly update the materials of glyphs,
    // even though we individually mark dirty materials. Used to work in Qt 5.5,
    // though.
    glyphsNode->markDirty(QSGNode::DirtyForceUpdate);
}
*/

/*
void Scatterplot::updateBrush(QSGNode *node)
{
    QMatrix4x4 transform;
    if (m_brushedItem < 0
        || (m_interactionState != StateNone
            && m_interactionState != StateSelected)) {
        transform.translate(-width(), -height());
    } else {
        const arma::rowvec &row = m_xy.row(m_brushedItem);
        transform.translate(m_sx(row[0]), m_sy(row[1]));
    }

    QSGTransformNode *brushNode = static_cast<QSGTransformNode *>(node);
    brushNode->setMatrix(transform);
}
*/

/*
void Scatterplot::mousePressEvent(QMouseEvent *event)
{
    switch (m_interactionState) {
    case StateNone:
    case StateSelected:
        switch (event->button()) {
        case NORMAL_BUTTON:
            if (event->modifiers() == Qt::ShiftModifier && m_dragEnabled) {
                m_interactionState = StateMoving;
                m_dragOriginPos = event->localPos();
                m_dragCurrentPos = m_dragOriginPos;
            } else {
                // We say 'brushing', but we mean 'selecting the current brushed
                // item'
                m_interactionState = StateBrushing;
            }
            break;
        case SPECIAL_BUTTON:
            m_interactionState = StateNone;
            m_selection.assign(m_selection.size(), false);
            selectionInteractivelyChanged(m_selection);
            m_shouldUpdateMaterials = true;
            update();
            break;
        }
        break;
    case StateBrushing:
    case StateSelecting:
    case StateMoving:
        // Probably shouldn't reach these
        break;
    }
}
*/

/*
void Scatterplot::mouseMoveEvent(QMouseEvent *event)
{
    switch (m_interactionState) {
    case StateBrushing:
        // Move while brushing becomes selecting, hence the 'fall through'
        m_interactionState = StateSelecting;
        m_dragOriginPos = event->localPos();
        // fall through
    case StateSelecting:
        m_dragCurrentPos = event->localPos();
        update();
        break;
    case StateMoving:
        m_dragCurrentPos = event->localPos();
        m_shouldUpdateGeometry = true;
        update();
        break;
    case StateNone:
    case StateSelected:
        break;
    }
}
*/

/*
void Scatterplot::mouseReleaseEvent(QMouseEvent *event)
{
    bool mergeSelection = (event->modifiers() == Qt::ControlModifier);

    switch (m_interactionState) {
    case StateBrushing:
        // Mouse clicked with brush target; set new selection or append to
        // current
        if (!mergeSelection) {
            m_selection.assign(m_selection.size(), false);
        }

        if (m_brushedItem == -1) {
            m_interactionState = StateNone;
            if (m_anySelected && !mergeSelection) {
                m_anySelected = false;
                selectionInteractivelyChanged(m_selection);
                m_shouldUpdateMaterials = true;
                update();
            }
        } else {
            m_interactionState = StateSelected;
            m_selection[m_brushedItem] = !m_selection[m_brushedItem];
            if (m_selection[m_brushedItem]) {
                m_anySelected = true;
            }

            selectionInteractivelyChanged(m_selection);
            m_shouldUpdateMaterials = true;
            update();
        }
        break;
    case StateSelecting:
        {
        // Selecting points and mouse is now released; update selection and
        // brush
        interactiveSelection(mergeSelection);
        m_interactionState = m_anySelected ? StateSelected : StateNone;
        QPoint pos = event->pos();
        m_brushedItem = m_quadtree->nearestTo(pos.x(), pos.y());

        itemInteractivelyBrushed(m_brushedItem);
        m_shouldUpdateMaterials = true;
        update();
        }
        break;
    case StateMoving:
        // Moving points and now stopped; apply manipulation
        m_interactionState = StateSelected;
        applyManipulation();
        m_shouldUpdateGeometry = true;
        update();

        m_dragOriginPos = m_dragCurrentPos;
        break;
    case StateNone:
    case StateSelected:
        break;
    }
}
*/

/*
void Scatterplot::hoverEnterEvent(QHoverEvent *event)
{
    QPointF pos = event->posF();
    m_brushedItem = m_quadtree->nearestTo(pos.x(), pos.y());
    itemInteractivelyBrushed(m_brushedItem);

    update();
}
*/

/*
void Scatterplot::hoverMoveEvent(QHoverEvent *event)
{
    QPointF pos = event->posF();
    m_brushedItem = m_quadtree->nearestTo(pos.x(), pos.y());
    itemInteractivelyBrushed(m_brushedItem);

    update();
}
*/

/*
void Scatterplot::hoverLeaveEvent(QHoverEvent *event)
{
    m_brushedItem = -1;
    itemInteractivelyBrushed(m_brushedItem);

    update();
}
*/

void Scatterplot::interactiveSelection(bool mergeSelection)
{
    if (!mergeSelection) {
        m_selection.assign(m_selection.size(), false);
    }

    std::vector<int> selected;
    m_quadtree->query(RectF(m_dragOriginPos, m_dragCurrentPos), selected);
    for (auto i: selected) {
        m_selection[i] = true;
    }

    for (auto isSelected: m_selection) {
        if (isSelected) {
            m_anySelected = true;
            break;
        }
    }

    selectionInteractivelyChanged(m_selection);
}

void Scatterplot::setSelection(const std::vector<bool> &selection)
{
    if (m_selection.size() != selection.size()) {
        return;
    }

    m_selection = selection;
    selectionChanged(m_selection);

    m_shouldUpdateMaterials = true;
    update();
}

void Scatterplot::brushItem(int item)
{
    m_brushedItem = item;
    update();
}

void Scatterplot::applyManipulation()
{
    m_sx.inverse();
    m_sy.inverse();
    LinearScale<float> rx = m_sx;
    LinearScale<float> ry = m_sy;
    m_sy.inverse();
    m_sx.inverse();

    float tx = m_dragCurrentPos.x - m_dragOriginPos.x;
    float ty = m_dragCurrentPos.y - m_dragOriginPos.y;

    for (std::vector<bool>::size_type i = 0; i < m_selection.size(); i++) {
        if (m_selection[i]) {
            arma::rowvec row = m_xy.row(i);
            row[0] = rx(m_sx(row[0]) + tx);
            row[1] = ry(m_sy(row[1]) + ty);
            m_xy.row(i) = row;
        }
    }

    updateQuadTree();

    xyInteractivelyChanged(m_xy);
}

void Scatterplot::updateQuadTree()
{
    m_sx.setRange(PADDING, m_width - PADDING);
    m_sy.setRange(m_height - PADDING, PADDING);

    m_quadtree.reset(new QuadTree(RectF(0.0f, 0.0f, m_width, m_height)));
    for (arma::uword i = 0; i < m_xy.n_rows; i++) {
        const arma::rowvec &row = m_xy.row(i);
        m_quadtree->insert(m_sx(row[0]), m_sy(row[1]), (int) i);
    }
}