aboutsummaryrefslogtreecommitdiff
#include <cmath>
#include <functional>
#include <iostream>
#include <memory>
#include <numeric>
#include <random>
#include <string>

#define GLAD_GL_IMPLEMENTATION
#include <glad/gl.h>
#undef GLAD_GL_IMPLEMENTATION
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
#define NK_INCLUDE_FONT_BAKING
#define NK_INCLUDE_DEFAULT_FONT
#define NK_SQRT sqrt
#define NK_SIN sinf
#define NK_COS cosf
// #define NK_STRTOD strtod
// #define NK_DTOA dtoa
#define NK_IMPLEMENTATION
#include "nuklear.h"

// device (NK) implementation details
static const int MAX_VERTEX_MEMORY = 512 * 1024;
static const int MAX_ELEMENT_MEMORY = 128 * 1024;

#ifdef __APPLE__
  #define NK_SHADER_VERSION "#version 150\n"
#else
  #define NK_SHADER_VERSION "#version 300 es\n"
#endif

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

#include "mp.h"
#include "continuouscolorscale.h"
#include "divergentcolorscale.h"
#include "numericrange.h"
#include "scatterplot.h"
#include "voronoisplat.h"
#include "barchart.h"
#include "colormap.h"
// #include "transitioncontrol.h"
#include "projectionhistory.h"
#include "manipulationhandler.h"
#include "mapscalehandler.h"
// #include "selectionhandler.h"
#include "brushinghandler.h"

static const int RNG_SEED = 123;
static const int WINDOW_WIDTH = 1000;
static const int WINDOW_HEIGHT = 800;
static const size_t PLOT_WIDTH = 512;
static const size_t PLOT_HEIGHT = 512;

static const std::vector<std::string> COLORMAP_NAMES{
    "Categorical",
    "Continuous",
    "Divergent",
    "Rainbow",
};

static const std::vector<std::string> METRIC_NAMES{
    "Aggregate error",
    "Ctrl. Pt. influence",
    "Stress",
};

struct media {
    struct nk_font *font_14;
    struct nk_font *font_18;
    struct nk_font *font_20;
    struct nk_font *font_22;

    struct nk_image unchecked;
    struct nk_image checked;
    struct nk_image rocket;
    struct nk_image cloud;
    struct nk_image pen;
    struct nk_image play;
    struct nk_image pause;
    struct nk_image stop;
    struct nk_image prev;
    struct nk_image next;
    struct nk_image tools;
    struct nk_image dir;
    struct nk_image copy;
    struct nk_image convert;
    struct nk_image del;
    struct nk_image edit;
    struct nk_image images[9];
    struct nk_image menu[6];
};
struct nk_glfw_vertex {
    float position[2];
    float uv[2];
    nk_byte col[4];
};

struct device {
    struct nk_buffer cmds;
    struct nk_draw_null_texture tex_null;
    GLuint vbo, vao, ebo;
    GLuint prog;
    GLuint vert_shdr;
    GLuint frag_shdr;
    GLint attrib_pos;
    GLint attrib_uv;
    GLint attrib_col;
    GLint uniform_tex;
    GLint uniform_proj;
    GLuint font_tex;
};

static void
ui_header(struct nk_context *ctx, struct media *media, const char *title)
{
    nk_style_set_font(ctx, &media->font_18->handle);
    nk_layout_row_dynamic(ctx, 20, 1);
    nk_label(ctx, title, NK_TEXT_LEFT);
}

static void
ui_widget(struct nk_context *ctx, struct media *media, float height)
{
    static const float ratio[] = {0.15f, 0.85f};
    nk_style_set_font(ctx, &media->font_22->handle);
    nk_layout_row(ctx, NK_DYNAMIC, height, 2, ratio);
    nk_spacing(ctx, 1);
}

static void
device_init(struct device *dev)
{
    GLint status;
    static const GLchar *vertex_shader =
        NK_SHADER_VERSION
        "uniform mat4 ProjMtx;\n"
        "in vec2 Position;\n"
        "in vec2 TexCoord;\n"
        "in vec4 Color;\n"
        "out vec2 Frag_UV;\n"
        "out vec4 Frag_Color;\n"
        "void main() {\n"
        "   Frag_UV = TexCoord;\n"
        "   Frag_Color = Color;\n"
        "   gl_Position = ProjMtx * vec4(Position.xy, 0, 1);\n"
        "}\n";
    static const GLchar *fragment_shader =
        NK_SHADER_VERSION
        "precision mediump float;\n"
        "uniform sampler2D Texture;\n"
        "in vec2 Frag_UV;\n"
        "in vec4 Frag_Color;\n"
        "out vec4 Out_Color;\n"
        "void main(){\n"
        "   Out_Color = Frag_Color * texture(Texture, Frag_UV.st);\n"
        "}\n";

    nk_buffer_init_default(&dev->cmds);
    dev->prog = glCreateProgram();
    dev->vert_shdr = glCreateShader(GL_VERTEX_SHADER);
    dev->frag_shdr = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(dev->vert_shdr, 1, &vertex_shader, 0);
    glShaderSource(dev->frag_shdr, 1, &fragment_shader, 0);
    glCompileShader(dev->vert_shdr);
    glCompileShader(dev->frag_shdr);
    glGetShaderiv(dev->vert_shdr, GL_COMPILE_STATUS, &status);
    assert(status == GL_TRUE);
    glGetShaderiv(dev->frag_shdr, GL_COMPILE_STATUS, &status);
    assert(status == GL_TRUE);
    glAttachShader(dev->prog, dev->vert_shdr);
    glAttachShader(dev->prog, dev->frag_shdr);
    glLinkProgram(dev->prog);
    glGetProgramiv(dev->prog, GL_LINK_STATUS, &status);
    assert(status == GL_TRUE);

    dev->uniform_tex = glGetUniformLocation(dev->prog, "Texture");
    dev->uniform_proj = glGetUniformLocation(dev->prog, "ProjMtx");
    dev->attrib_pos = glGetAttribLocation(dev->prog, "Position");
    dev->attrib_uv = glGetAttribLocation(dev->prog, "TexCoord");
    dev->attrib_col = glGetAttribLocation(dev->prog, "Color");

    {
        /* buffer setup */
        GLsizei vs = sizeof(struct nk_glfw_vertex);
        size_t vp = offsetof(struct nk_glfw_vertex, position);
        size_t vt = offsetof(struct nk_glfw_vertex, uv);
        size_t vc = offsetof(struct nk_glfw_vertex, col);

        glGenBuffers(1, &dev->vbo);
        glGenBuffers(1, &dev->ebo);
        glGenVertexArrays(1, &dev->vao);

        glBindVertexArray(dev->vao);
        glBindBuffer(GL_ARRAY_BUFFER, dev->vbo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, dev->ebo);

        glEnableVertexAttribArray((GLuint)dev->attrib_pos);
        glEnableVertexAttribArray((GLuint)dev->attrib_uv);
        glEnableVertexAttribArray((GLuint)dev->attrib_col);

        glVertexAttribPointer((GLuint)dev->attrib_pos, 2, GL_FLOAT, GL_FALSE, vs, (void*)vp);
        glVertexAttribPointer((GLuint)dev->attrib_uv, 2, GL_FLOAT, GL_FALSE, vs, (void*)vt);
        glVertexAttribPointer((GLuint)dev->attrib_col, 4, GL_UNSIGNED_BYTE, GL_TRUE, vs, (void*)vc);
    }

    glBindTexture(GL_TEXTURE_2D, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

static void
device_upload_atlas(struct device *dev, const void *image, int width, int height)
{
    glGenTextures(1, &dev->font_tex);
    glBindTexture(GL_TEXTURE_2D, dev->font_tex);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)width, (GLsizei)height, 0,
                GL_RGBA, GL_UNSIGNED_BYTE, image);
}

static void
device_shutdown(struct device *dev)
{
    glDetachShader(dev->prog, dev->vert_shdr);
    glDetachShader(dev->prog, dev->frag_shdr);
    glDeleteShader(dev->vert_shdr);
    glDeleteShader(dev->frag_shdr);
    glDeleteProgram(dev->prog);
    glDeleteTextures(1, &dev->font_tex);
    glDeleteBuffers(1, &dev->vbo);
    glDeleteBuffers(1, &dev->ebo);
    nk_buffer_free(&dev->cmds);
}

static void
device_draw(struct device *dev, struct nk_context *ctx, int width, int height,
    struct nk_vec2 scale, enum nk_anti_aliasing AA)
{
    GLfloat ortho[4][4] = {
        {2.0f, 0.0f, 0.0f, 0.0f},
        {0.0f,-2.0f, 0.0f, 0.0f},
        {0.0f, 0.0f,-1.0f, 0.0f},
        {-1.0f,1.0f, 0.0f, 1.0f},
    };
    ortho[0][0] /= (GLfloat)width;
    ortho[1][1] /= (GLfloat)height;

    /* setup global state */
    glEnable(GL_BLEND);
    glBlendEquation(GL_FUNC_ADD);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glDisable(GL_CULL_FACE);
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_SCISSOR_TEST);
    glActiveTexture(GL_TEXTURE0);

    /* setup program */
    glUseProgram(dev->prog);
    glUniform1i(dev->uniform_tex, 0);
    glUniformMatrix4fv(dev->uniform_proj, 1, GL_FALSE, &ortho[0][0]);
    {
        /* convert from command queue into draw list and draw to screen */
        const struct nk_draw_command *cmd;
        void *vertices, *elements;
        const nk_draw_index *offset = NULL;

        /* allocate vertex and element buffer */
        glBindVertexArray(dev->vao);
        glBindBuffer(GL_ARRAY_BUFFER, dev->vbo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, dev->ebo);

        glBufferData(GL_ARRAY_BUFFER, MAX_VERTEX_MEMORY, NULL, GL_STREAM_DRAW);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, MAX_ELEMENT_MEMORY, NULL, GL_STREAM_DRAW);

        /* load draw vertices & elements directly into vertex + element buffer */
        vertices = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
        elements = glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY);
        {
            /* fill convert configuration */
            struct nk_convert_config config;
            static const struct nk_draw_vertex_layout_element vertex_layout[] = {
                {NK_VERTEX_POSITION, NK_FORMAT_FLOAT, NK_OFFSETOF(struct nk_glfw_vertex, position)},
                {NK_VERTEX_TEXCOORD, NK_FORMAT_FLOAT, NK_OFFSETOF(struct nk_glfw_vertex, uv)},
                {NK_VERTEX_COLOR, NK_FORMAT_R8G8B8A8, NK_OFFSETOF(struct nk_glfw_vertex, col)},
                {NK_VERTEX_LAYOUT_END}
            };
            NK_MEMSET(&config, 0, sizeof(config));
            config.vertex_layout = vertex_layout;
            config.vertex_size = sizeof(struct nk_glfw_vertex);
            config.vertex_alignment = NK_ALIGNOF(struct nk_glfw_vertex);
            config.tex_null = dev->tex_null;
            config.circle_segment_count = 22;
            config.curve_segment_count = 22;
            config.arc_segment_count = 22;
            config.global_alpha = 1.0f;
            config.shape_AA = AA;
            config.line_AA = AA;

            /* setup buffers to load vertices and elements */
            {struct nk_buffer vbuf, ebuf;
            nk_buffer_init_fixed(&vbuf, vertices, MAX_VERTEX_MEMORY);
            nk_buffer_init_fixed(&ebuf, elements, MAX_ELEMENT_MEMORY);
            nk_convert(ctx, &dev->cmds, &vbuf, &ebuf, &config);}
        }
        glUnmapBuffer(GL_ARRAY_BUFFER);
        glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);

        /* iterate over and execute each draw command */
        nk_draw_foreach(cmd, ctx, &dev->cmds)
        {
            if (!cmd->elem_count) continue;
            glBindTexture(GL_TEXTURE_2D, (GLuint)cmd->texture.id);
            glScissor(
                (GLint)(cmd->clip_rect.x * scale.x),
                (GLint)((height - (GLint)(cmd->clip_rect.y + cmd->clip_rect.h)) * scale.y),
                (GLint)(cmd->clip_rect.w * scale.x),
                (GLint)(cmd->clip_rect.h * scale.y));
            glDrawElements(GL_TRIANGLES, (GLsizei)cmd->elem_count, GL_UNSIGNED_SHORT, offset);
            offset += cmd->elem_count;
        }
        nk_clear(ctx);
        nk_buffer_clear(&dev->cmds);
    }

    /* default OpenGL state */
    glUseProgram(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    glDisable(GL_BLEND);
    glDisable(GL_SCISSOR_TEST);
}

class Main
{
public:
    Main(Main const&) = delete;
    void operator=(Main const&) = delete;

    static Main &instance() {
        static Main instance;
        return instance;
    }

    bool saveData() const {
        bool ret = true;
        if (m_cp.n_elem > 0 && m_indicesSavePath.size() > 0) {
            ret = ret && m_cp.save(m_cpSavePath, arma::raw_ascii);
        }
        if (m_cpIndices.n_elem > 0 && m_cpSavePath.size() > 0) {
            ret = ret && m_cpIndices.save(m_indicesSavePath, arma::raw_ascii);
        }

        return ret;
    }

    bool loadDataset(const std::string &path) {
        return m_X.load(path, arma::raw_ascii);
    }

    void setIndicesSavePath(const std::string &path) {
        m_indicesSavePath = path;
    }

    void setCPSavePath(const std::string &path) {
        m_cpSavePath = path;
    }

    arma::mat X() const { return m_X; }

    void setSelectRPs() {
        // cpPlot->setAcceptedMouseButtons(Qt::NoButton);
        // cpPlot->setAcceptHoverEvents(false);

        // rpPlot->setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton);
        // rpPlot->setAcceptHoverEvents(true);
    }

    void setSelectCPs() {
        // rpPlot->setAcceptedMouseButtons(Qt::NoButton);
        // rpPlot->setAcceptHoverEvents(false);

        // cpPlot->setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton);
        // cpPlot->setAcceptHoverEvents(true);
    }

    enum ColorScaleType {
        ColorScaleCategorical = 0,
        ColorScaleContinuous,
        ColorScaleDivergent,
        ColorScaleRainbow
    };

    void setCPColorScale(ColorScaleType colorScaleType) {
        float min = 0.0f;
        float max = 1.0f;
        if (colorScaleCPs) {
            min = colorScaleCPs->min();
            max = colorScaleCPs->max();
        }
        colorScaleCPs = getColorScale(colorScaleType);
        colorScaleCPs->setExtents(min, max);

        // cpBarChart->setColorScale(colorScaleCPs);
        cpColormap->setColorScale(colorScaleCPs);
        // bundlePlot->setColorScale(colorScaleCPs);
        cpPlot->update();
        cpBarChart->update();
    }

    void setRPColorScale(ColorScaleType colorScaleType) {
        float min = 0.0f;
        float max = 1.0f;
        // TODO: if weird behaviors are found, check this (was CPs)
        if (colorScaleRPs) {
            min = colorScaleRPs->min();
            max = colorScaleRPs->max();
        }
        colorScaleRPs = getColorScale(colorScaleType);
        colorScaleRPs->setExtents(min, max);

        rpColormap->setColorScale(colorScaleRPs);
        rpPlot->update();
        splat->update();
        rpBarChart->update();
    }

    // Pointers to visual components whose values are set in the main() function
    // after components are instantiated by the QtQuick engine
    BarChart *cpBarChart, *rpBarChart;
    Colormap *cpColormap, *rpColormap;
    Scatterplot *cpPlot, *rpPlot;
    VoronoiSplat *splat;
    // LinePlot *bundlePlot;

    // Color scales in use
    std::shared_ptr<ColorScale> colorScaleCPs, colorScaleRPs;

    // Object that controls manipulation history
    ProjectionHistory *projectionHistory;

    void undoManipulation()  { projectionHistory->undo(); }
    void resetManipulation() { projectionHistory->reset(); }

    enum ObserverType {
        ObserverCurrent      = ProjectionHistory::ObserverCurrent,
        ObserverDiffPrevious = ProjectionHistory::ObserverDiffPrevious,
        ObserverDiffFirst    = ProjectionHistory::ObserverDiffFirst
    };

    bool setObserverType(ObserverType observerType) {
        switch (observerType) {
        case ObserverCurrent:
            return projectionHistory->setType(ProjectionHistory::ObserverCurrent);
        case ObserverDiffPrevious:
            return projectionHistory->setType(ProjectionHistory::ObserverDiffPrevious);
        case ObserverDiffFirst:
            return projectionHistory->setType(ProjectionHistory::ObserverDiffFirst);
        }

        return false;
    }

    void setCPIndices(const arma::uvec &indices) {
        m_cpIndices = indices;

        m_rpIndices.set_size(m_X.n_rows - m_cpIndices.n_elem);
        NumericRange<arma::uword> allIndices(0, m_X.n_rows);
        std::set_symmetric_difference(allIndices.cbegin(), allIndices.cend(),
                m_cpIndices.cbegin(), m_cpIndices.cend(), m_rpIndices.begin());
    }

    void setCP(const arma::mat &cp) {
        if (cp.n_cols != 2
            || cp.n_rows != m_cpIndices.n_elem) {
            return;
        }

        m_cp = cp;
    }

    void updateMap(const arma::mat &Y) {
        cpPlot->setXY(Y.rows(m_cpIndices));

        const arma::mat &regularPoints = Y.rows(m_rpIndices);
        rpPlot->setXY(regularPoints);
        splat->setSites(regularPoints);
    }

private:
    Main()
        : cpPlot(0)
        , rpPlot(0)
        , splat(0)
        , cpBarChart(0)
        , rpBarChart(0)
        , cpColormap(0)
        , rpColormap(0)
        // , bundlePlot(0)
        , projectionHistory(0)
    {
    }

    std::shared_ptr<ColorScale> getColorScale(ColorScaleType colorScaleType) {
        switch (colorScaleType) {
        case ColorScaleCategorical:
            return std::shared_ptr<ColorScale>(new ColorScale{
                    // QColor("#1f77b4"), QColor("#ff7f0e"), QColor("#2ca02c"),
                    Color(31, 119, 180), Color(255, 127, 14), Color(44, 160, 44),
                    // QColor("#d62728"), QColor("#9467bd"), QColor("#8c564b"),
                    Color(214, 39, 40), Color(148, 103, 189), Color(140, 86, 75),
                    // QColor("#e377c2"), QColor("#17becf"), QColor("#7f7f7f"),
                    Color(227, 119, 194), Color(23, 190, 207), Color(127, 127, 127),
                });
        case ColorScaleContinuous:
            return std::shared_ptr<ColorScale>(
                ContinuousColorScale::builtin(
                    ContinuousColorScale::HeatedObjects, nullptr));
        case ColorScaleDivergent:
            return std::shared_ptr<ColorScale>(
                DivergentColorScale::builtin(
                    DivergentColorScale::RedGrayBlue, nullptr));
        case ColorScaleRainbow:
            // fall-through
        default:
            return std::shared_ptr<ColorScale>(
                ContinuousColorScale::builtin(
                    ContinuousColorScale::Rainbow, nullptr));
        }
    }

    arma::mat m_X, m_cp;
    arma::uvec m_cpIndices, m_rpIndices;
    std::string m_indicesSavePath, m_cpSavePath;
};

static void checkGLError(const char *msg) {
    GLenum error = glGetError();
    std::cout << msg << " ";
    switch (error) {
    case GL_NO_ERROR:
        std::cout << "GL_NO_ERROR" << std::endl;
        break;
    case GL_INVALID_ENUM:
        std::cout << "GL_INVALID_ENUM" << std::endl;
        break;
    case GL_INVALID_VALUE:
        std::cout << "GL_INVALID_VALUE" << std::endl;
        break;
    case GL_INVALID_OPERATION:
        std::cout << "GL_INVALID_OPERATION" << std::endl;
        break;
    case GL_INVALID_FRAMEBUFFER_OPERATION:
        std::cout << "GL_INVALID_FRAMEBUFFER_OPERATION" << std::endl;
        break;
    case GL_OUT_OF_MEMORY:
        std::cout << "GL_OUT_OF_MEMORY" << std::endl;
        break;
    }
}

arma::uvec extractCPs(const arma::mat &X)
{
    int numCPs = (int) (3 * sqrt(X.n_rows));
    arma::uvec indices(X.n_rows);
    std::iota(indices.begin(), indices.end(), 0);
    indices = arma::shuffle(indices);
    return indices.subvec(0, numCPs-1);
}

arma::mat standardise(const arma::mat &X)
{
    arma::mat stdX = X;
    for (arma::uword j = 0; j < X.n_cols; j++) {
        double sd = arma::stddev(stdX.col(j));
        double mean = arma::mean(stdX.col(j));
        stdX.col(j) = (stdX.col(j) - mean) / sd;
    }

    return stdX;
}

// GLFW callbacks
static void glfw_errorCallback(int err, const char *description)
{
    std::cerr << "glfw: Error: " << err << ": " << description << std::endl;
}

static void text_input(GLFWwindow *win, unsigned int codepoint)
{
    nk_input_unicode((struct nk_context *) glfwGetWindowUserPointer(win), codepoint);
}

static void scroll_input(GLFWwindow *win, double _, double yoff)
{
    (void) _;
    nk_input_scroll((struct nk_context *) glfwGetWindowUserPointer(win),
                    nk_vec2(0, (float) yoff));
}

int main(int argc, char *argv[])
{
    static GLFWwindow *win;
    int width = 0, height = 0;
    int display_width = 0, display_height = 0;

    // GUI
    struct device device;
    struct nk_font_atlas atlas;
    struct media media;
    struct nk_context ctx;

    NK_UNUSED(argc);
    NK_UNUSED(argv);

    glfwSetErrorCallback(glfw_errorCallback);
    if (!glfwInit()) {
        std::cerr << "glfw: init failed" << std::endl;
        exit(1);
    }

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    glfwWindowHint(GLFW_SCALE_TO_MONITOR, GL_TRUE);
#endif
    glfwWindowHint(GLFW_RED_BITS, 8);
    glfwWindowHint(GLFW_GREEN_BITS, 8);
    glfwWindowHint(GLFW_BLUE_BITS, 8);
    glfwWindowHint(GLFW_ALPHA_BITS, 8);
    glfwWindowHint(GLFW_SAMPLES, 4);

    win = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "pm", NULL, NULL);
    if (!win) {
        std::cerr << "glfw: failed to create window" << std::endl;
        glfwTerminate();
        exit(1);
    }

    glfwMakeContextCurrent(win);
    glfwSetWindowUserPointer(win, &ctx);
    glfwSetCharCallback(win, text_input);
    glfwSetScrollCallback(win, scroll_input);
    glfwGetWindowSize(win, &width, &height);
    glfwGetFramebufferSize(win, &display_width, &display_height);

    GLFWmonitor *primary = glfwGetPrimaryMonitor();
    float xscale, yscale;
    glfwGetMonitorContentScale(primary, &xscale, &yscale);
    std::cout << "glfw: Detected scaling: "
              << xscale << "x" << yscale
              << std::endl;
    const GLFWvidmode *mode = glfwGetVideoMode(primary);
    std::cout << "glfw: Detected resolution: "
              << mode->width << "x" << mode->height
              << std::endl;

    if (!gladLoadGL(glfwGetProcAddress)) {
        std::cerr << "GLAD load failed" << std::endl;
        glfwTerminate();
        exit(1);
    }

    glEnable(GL_MULTISAMPLE);
    glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);

    Main &m = Main::instance();
    if (argc != 2 || !m.loadDataset(argv[1])) {
        std::cerr << "Could not load data." << std::endl;
        return 1;
    }

    const arma::mat &X = standardise(m.X());

    arma::arma_rng::set_seed(RNG_SEED);
    arma::uvec cpIndices;
    arma::mat Ys;

    // Load/generate control point indices
    cpIndices = extractCPs(X);

    // Load/generate control points
    Ys.set_size(cpIndices.n_elem, 2);
    Ys.randn();
    mp::forceScheme(mp::dist(X.rows(cpIndices)), Ys);

    if (cpIndices.n_elem != Ys.n_rows) {
        std::cerr << "The number of CP indices and the CP map do not match." << std::endl;
        return 1;
    }

    // TODO: maybe put this inside m->setCP() method; for now, will be outside
    // for more flexibility
    // Ys = arma::normalise(Ys);

    // Sort indices (some operations become easier later)
    arma::uvec cpSortedIndices = arma::sort_index(cpIndices);
    cpIndices = cpIndices(cpSortedIndices);
    Ys = Ys.rows(cpSortedIndices);

    m.setCPIndices(cpIndices);
    m.setCP(Ys);

    // Keeps track of what happened to the visualization
    ProjectionHistory history(X, cpIndices);

    // Visual components
    Colormap cpColormap, rpColormap;
    // cpColormap.setOrientation(Colormap::Vertical);
    // rpColormap.setOrientation(Colormap::Vertical);
    Scatterplot cpPlot, rpPlot;
    VoronoiSplat splat;
    cpPlot.setSize(PLOT_WIDTH, PLOT_HEIGHT);
    rpPlot.setSize(PLOT_WIDTH, PLOT_HEIGHT);
    rpPlot.setGlyphSize(3.0f);
    rpPlot.setColormap(rpColormap.texture());
    splat.setSize(PLOT_WIDTH, PLOT_HEIGHT);
    splat.setColormap(rpColormap.texture());
    BarChart cpBarChart, rpBarChart;
    cpBarChart.setSize(PLOT_WIDTH, 60);
    cpBarChart.setColormap(cpColormap.texture());
    rpBarChart.setSize(PLOT_WIDTH, 60);
    rpBarChart.setColormap(rpColormap.texture());
    m.cpPlot = &cpPlot;
    m.rpPlot = &rpPlot;
    m.splat = &splat;
    m.cpColormap = &cpColormap;
    m.rpColormap = &rpColormap;
    m.cpBarChart = &cpBarChart;
    m.rpBarChart = &rpBarChart;

    auto setCP = std::bind(&Main::setCP, &m, std::placeholders::_1);
    cpPlot.xyChanged.connect(setCP);
    cpPlot.xyInteractivelyChanged.connect(setCP);

    // Keep both scatterplots and the splat scaled equally and
    // relative to the full plot
    MapScaleHandler mapScaleHandler;
    mapScaleHandler.scaleChanged.connect(
        std::bind(&Scatterplot::setScale, &cpPlot, std::placeholders::_1, std::placeholders::_2));
    mapScaleHandler.scaleChanged.connect(
        std::bind(&Scatterplot::setScale, &rpPlot, std::placeholders::_1, std::placeholders::_2));
    mapScaleHandler.scaleChanged.connect(
        std::bind(&VoronoiSplat::setScale, &splat, std::placeholders::_1, std::placeholders::_2));
    history.currentMapChanged.connect(
        std::bind(&MapScaleHandler::scaleToMap, &mapScaleHandler, std::placeholders::_1));

    // Update projection as the cp are modified (either directly in
    // the manipulationHandler object or interactively in cpPlot
    ManipulationHandler manipulationHandler(X, cpIndices);
    cpPlot.xyInteractivelyChanged.connect(
        std::bind(&ManipulationHandler::setCP, &manipulationHandler, std::placeholders::_1));
    // Update history whenever a new projection is computed...
    manipulationHandler.mapChanged.connect(
        std::bind(&ProjectionHistory::addMap, &history, std::placeholders::_1));
    // ... and update visual components whenever the history changes
    history.currentMapChanged.connect(
        std::bind(&Main::updateMap, &m, std::placeholders::_1));

    // Linking between selections
    /*
    SelectionHandler cpSelectionHandler(cpIndices.n_elem);
    connect(m->cpPlot, &Scatterplot::selectionInteractivelyChanged,
            &cpSelectionHandler, &SelectionHandler::setSelection);
    connect(m->cpBarChart, &BarChart::selectionInteractivelyChanged,
            &cpSelectionHandler, &SelectionHandler::setSelection);
    connect(&cpSelectionHandler, &SelectionHandler::selectionChanged,
            m->cpPlot, &Scatterplot::setSelection);
    connect(&cpSelectionHandler, &SelectionHandler::selectionChanged,
            m->cpBarChart, &BarChart::setSelection);
    connect(&cpSelectionHandler, &SelectionHandler::selectionChanged,
            [m](const std::vector<bool> &cpSelection) {
                // given some CPs, see unexpected RPs *influenced by* them
                std::vector<arma::uword> selectedCPIndices;
                for (int i = 0; i < cpSelection.size(); i++) {
                    if (cpSelection[i]) {
                        selectedCPIndices.push_back(i);
                    }
                }

                if (selectedCPIndices.empty()) {
                    return;
                }

                arma::uvec selectedCPs(selectedCPIndices.size());
                std::copy(selectedCPIndices.begin(), selectedCPIndices.end(),
                        selectedCPs.begin());

                // Only the 10% largest values, filtered by the selected RPs
                const arma::mat &unreliability = m->projectionHistory->unreliability();
                arma::uvec indicesLargest = arma::sort_index(unreliability.cols(selectedCPs), "descending");
                arma::uword numLargest = indicesLargest.n_elem * 0.01f;
                indicesLargest = indicesLargest.subvec(0, numLargest-1);
                // m->bundlePlot->setValues(unreliability(indicesLargest));

                const arma::uvec &cpIndices = m->projectionHistory->cpIndices();
                arma::uvec CPs = cpIndices(selectedCPs(indicesLargest / unreliability.n_rows));

                const arma::uvec &rpIndices = m->projectionHistory->rpIndices();
                arma::uvec RPs = indicesLargest;
                RPs.transform([&unreliability](arma::uword v) {
                    return v % unreliability.n_rows;
                });
                RPs = rpIndices(RPs);

                arma::uvec indices(CPs.n_elem + RPs.n_elem);
                for (arma::uword i = 0; i < CPs.n_elem; i++) {
                    indices[2*i + 0] = CPs(i);
                    indices[2*i + 1] = RPs(i);
                }
                // m->bundlePlot->setLines(indices, m->projectionHistory->Y());
            });
    */

    // Same as before, but now for regular points
    /*
    SelectionHandler rpSelectionHandler(X.n_rows - cpIndices.n_elem);
    rpPlot.selectionInteractivelyChanged.connect(
        std::bind(&SelectionHandler::setSelection, &rpSelectionHandler));
    rpBarChart.selectionInteractivelyChanged.connect(
        std::bind(&SelectionHandler::setSelection, &rpSelectionHandler));
    rpSelectionHandler.selectionChanged.connect(
        std::bind(&Scatterplot::setSelection, &rpPlot));
    rpSelectionHandler.selectionChanged.connect(
        std::bind(&BarChart::setSelection, &rpBarChart));
    // This still needs more tests
    QObject::connect(&rpSelectionHandler, &SelectionHandler::selectionChanged,
            [m](const std::vector<bool> &rpSelection) {
                // given some RPs, see unexpected CPs *influencing* them
                std::vector<arma::uword> selectedRPIndices;
                for (int i = 0; i < rpSelection.size(); i++) {
                    if (rpSelection[i]) {
                        selectedRPIndices.push_back(i);
                    }
                }

                if (selectedRPIndices.empty()) {
                    overviewBundles(m);
                    return;
                }

                arma::uvec selectedRPs(selectedRPIndices.size());
                std::copy(selectedRPIndices.begin(), selectedRPIndices.end(),
                        selectedRPs.begin());

                // Only the 10% largest values, filtered by the selected RPs
                const arma::mat &unreliability = m->projectionHistory->unreliability();
                arma::uvec indicesLargest = arma::sort_index(unreliability.rows(selectedRPs), "descending");
                arma::uword numLargest = indicesLargest.n_elem * 0.01f;
                indicesLargest = indicesLargest.subvec(0, numLargest-1);
                m->bundlePlot->setValues(unreliability(indicesLargest));

                const arma::uvec &cpIndices = m->projectionHistory->cpIndices();
                arma::uvec CPs = cpIndices(indicesLargest / selectedRPs.n_elem);

                const arma::uvec &rpIndices = m->projectionHistory->rpIndices();
                arma::uvec RPs = indicesLargest;
                RPs.transform([&selectedRPs](arma::uword v) {
                    return v % selectedRPs.n_elem;
                });
                RPs = rpIndices(selectedRPs(RPs));

                arma::uvec indices(CPs.n_elem + RPs.n_elem);
                for (arma::uword i = 0; i < CPs.n_elem; i++) {
                    indices[2*i + 0] = CPs(i);
                    indices[2*i + 1] = RPs(i);
                }
                m->bundlePlot->setLines(indices, m->projectionHistory->Y());
            });
    */

    // Brushing between each bar chart and respective scatterplot
    BrushingHandler cpBrushHandler;
    auto cpBrushHandler_brushItem = std::bind(
        &BrushingHandler::brushItem,
        &cpBrushHandler,
        std::placeholders::_1);
    cpPlot.itemInteractivelyBrushed.connect(cpBrushHandler_brushItem);
    cpBarChart.itemInteractivelyBrushed.connect(cpBrushHandler_brushItem);
    cpBrushHandler.itemBrushed.connect(
        std::bind(&Scatterplot::brushItem, &cpPlot, std::placeholders::_1));
    cpBrushHandler.itemBrushed.connect(
        std::bind(&BarChart::brushItem, &cpBarChart, std::placeholders::_1));

    BrushingHandler rpBrushHandler;
    auto rpBrushHandler_brushItem = std::bind(
        &BrushingHandler::brushItem,
        &rpBrushHandler,
        std::placeholders::_1);
    rpPlot.itemInteractivelyBrushed.connect(rpBrushHandler_brushItem);
    rpBarChart.itemInteractivelyBrushed.connect(rpBrushHandler_brushItem);
    rpBrushHandler.itemBrushed.connect(
        std::bind(&Scatterplot::brushItem, &rpPlot, std::placeholders::_1));
    rpBrushHandler.itemBrushed.connect(
        std::bind(&BarChart::brushItem, &rpBarChart, std::placeholders::_1));

    // Update visual components whenever values change
    history.rpValuesChanged.connect([&](const arma::vec &v, bool rescale) {
        if (!m.colorScaleRPs || v.n_elem == 0) {
            return;
        }

        if (rescale) {
            m.colorScaleRPs->setExtents(v.min(), v.max());
        } else {
            m.colorScaleRPs->setExtents(std::min(m.colorScaleRPs->min(), (float) v.min()),
                                         std::max(m.colorScaleRPs->max(), (float) v.max()));
        }

        m.rpColormap->setColorScale(m.colorScaleRPs);
        // rpPlot.setColorData(v);
    });
    history.cpValuesChanged.connect([&](const arma::vec &v, bool rescale) {
        if (!m.colorScaleCPs || v.n_elem == 0) {
            return;
        }

        m.colorScaleCPs->setExtents(v.min(), v.max());
        m.cpColormap->setColorScale(m.colorScaleCPs);

        // Originally, we should have done this:
        //
        //    history.cpValuesChanged.connect(
        //        std::bind(&Scatterplot::setColorData, &cpPlot, std::placeholders::_1));
        //
        // But this has problems with the number of arguments of
        // setColorData, so we just call it here
        cpPlot.setColorData(v);
    });
    history.cpValuesChanged.connect(
        std::bind(&BarChart::setValues, &cpBarChart, std::placeholders::_1));
    history.rpValuesChanged.connect(
        std::bind(&VoronoiSplat::setValues, &splat, std::placeholders::_1));
    history.rpValuesChanged.connect(
        std::bind(&BarChart::setValues, &rpBarChart, std::placeholders::_1));

    // ProjectionHistory takes special care of separate CP/RP selections
    // cpSelectionHandler.selectionChanged.connect(
    //     std::bind(&ProjectionHistory::setCPSelection, &history));
    // rpSelectionHandler.selectionChanged.connect(
    //     std::bind(&ProjectionHistory::setRPSelection, &history));
    // history.selectionChanged.connect(
    //     std::bind(&LinePlot::selectionChanged, &bundlePlot));

    // Connect projection components to rewinding mechanism
    // TransitionControl plotTC;
    // plotTC.tChanged.connect(
    //     std::bind(&ProjectionHistory::setRewind, &history, std::placeholders::_1));
    history.mapRewound.connect(
        std::bind(&Main::updateMap, &m, std::placeholders::_1));
    history.cpValuesRewound.connect(
        std::bind(&Scatterplot::setColorData, &cpPlot, std::placeholders::_1));
    history.rpValuesRewound.connect(
        std::bind(&VoronoiSplat::setValues, &splat, std::placeholders::_1));
    history.rpValuesRewound.connect(
        std::bind(&BarChart::updateValues, &rpBarChart, std::placeholders::_1));

    // General component set up
    // plotTC->setAcceptedMouseButtons(Qt::RightButton);
    // m->cpPlot->setDragEnabled(true);
    // m->cpBarChart->setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton);
    m.setSelectCPs();

    // m->cpBarChart->setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton);
    // m->rpBarChart->setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton);

    m.setCPColorScale(Main::ColorScaleRainbow);
    enum Main::ColorScaleType cpColorScaleType = Main::ColorScaleRainbow;
    m.setRPColorScale(Main::ColorScaleRainbow);
    enum Main::ColorScaleType rpColorScaleType = Main::ColorScaleRainbow;

    // This sets the initial CP configuration, triggering all the necessary
    // signals to set up the helper objects and visual components
    manipulationHandler.setCP(Ys);


    // GUI init
    device_init(&device);
    {
        const void *image;
        int w, h;
        struct nk_font_config cfg = nk_font_config(0);
        cfg.oversample_h = 3;
        cfg.oversample_v = 2;
        nk_font_atlas_init_default(&atlas);
        nk_font_atlas_begin(&atlas);
        media.font_14 = nk_font_atlas_add_from_file(&atlas, "assets/Roboto-Regular.ttf", 14.0f, &cfg);
        media.font_18 = nk_font_atlas_add_from_file(&atlas, "assets/Roboto-Regular.ttf", 18.0f, &cfg);
        media.font_20 = nk_font_atlas_add_from_file(&atlas, "assets/Roboto-Regular.ttf", 20.0f, &cfg);
        media.font_22 = nk_font_atlas_add_from_file(&atlas, "assets/Roboto-Regular.ttf", 22.0f, &cfg);
        image = nk_font_atlas_bake(&atlas, &w, &h, NK_FONT_ATLAS_RGBA32);
        device_upload_atlas(&device, image, w, h);
        nk_font_atlas_end(&atlas, nk_handle_id((int)device.font_tex), &device.tex_null);
    }
    nk_init_default(&ctx, &media.font_14->handle);

    struct nk_image cpPlot_img = nk_image_id((int) cpPlot.texture());
    struct nk_image rpPlot_img = nk_image_id((int) rpPlot.texture());
    struct nk_image splat_img = nk_image_id((int) splat.texture());
    struct nk_image cpColormap_img = nk_image_id((int) cpColormap.texture());
    struct nk_image rpColormap_img = nk_image_id((int) rpColormap.texture());
    struct nk_image cpBarChart_img = nk_image_id((int) cpBarChart.texture());
    struct nk_image rpBarChart_img = nk_image_id((int) rpBarChart.texture());
    while (!glfwWindowShouldClose(win)) {
        struct nk_vec2 scale;
        glfwGetWindowSize(win, &width, &height);
        glfwGetFramebufferSize(win, &display_width, &display_height);
        scale.x = static_cast<float>(display_width) / static_cast<float>(width);
        scale.y = static_cast<float>(display_height) / static_cast<float>(height);

        // Input
        glfwPollEvents();
        {
            double x, y;
            nk_input_begin(&ctx);
            // glfwPollEvents();
            nk_input_key(&ctx, NK_KEY_DEL, glfwGetKey(win, GLFW_KEY_DELETE) == GLFW_PRESS);
            nk_input_key(&ctx, NK_KEY_ENTER, glfwGetKey(win, GLFW_KEY_ENTER) == GLFW_PRESS);
            nk_input_key(&ctx, NK_KEY_TAB, glfwGetKey(win, GLFW_KEY_TAB) == GLFW_PRESS);
            nk_input_key(&ctx, NK_KEY_BACKSPACE, glfwGetKey(win, GLFW_KEY_BACKSPACE) == GLFW_PRESS);
            nk_input_key(&ctx, NK_KEY_LEFT, glfwGetKey(win, GLFW_KEY_LEFT) == GLFW_PRESS);
            nk_input_key(&ctx, NK_KEY_RIGHT, glfwGetKey(win, GLFW_KEY_RIGHT) == GLFW_PRESS);
            nk_input_key(&ctx, NK_KEY_UP, glfwGetKey(win, GLFW_KEY_UP) == GLFW_PRESS);
            nk_input_key(&ctx, NK_KEY_DOWN, glfwGetKey(win, GLFW_KEY_DOWN) == GLFW_PRESS);
            if (glfwGetKey(win, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS ||
                glfwGetKey(win, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS) {
                nk_input_key(&ctx, NK_KEY_COPY, glfwGetKey(win, GLFW_KEY_C) == GLFW_PRESS);
                nk_input_key(&ctx, NK_KEY_PASTE, glfwGetKey(win, GLFW_KEY_P) == GLFW_PRESS);
                nk_input_key(&ctx, NK_KEY_CUT, glfwGetKey(win, GLFW_KEY_X) == GLFW_PRESS);
                nk_input_key(&ctx, NK_KEY_CUT, glfwGetKey(win, GLFW_KEY_E) == GLFW_PRESS);
                nk_input_key(&ctx, NK_KEY_SHIFT, 1);
            } else {
                nk_input_key(&ctx, NK_KEY_COPY, 0);
                nk_input_key(&ctx, NK_KEY_PASTE, 0);
                nk_input_key(&ctx, NK_KEY_CUT, 0);
                nk_input_key(&ctx, NK_KEY_SHIFT, 0);
            }
            glfwGetCursorPos(win, &x, &y);
            int int_x = static_cast<int>(x);
            int int_y = static_cast<int>(y);
            nk_input_motion(&ctx, int_x, int_y);
            nk_input_button(&ctx, NK_BUTTON_LEFT, int_x, int_y,
                            glfwGetMouseButton(win, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS);
            nk_input_button(&ctx, NK_BUTTON_MIDDLE, int_x, int_y,
                            glfwGetMouseButton(win, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS);
            nk_input_button(&ctx, NK_BUTTON_RIGHT, int_x, int_y,
                            glfwGetMouseButton(win, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS);
            nk_input_end(&ctx);
        }

        nk_style_set_font(&ctx, &media.font_20->handle);
        if (nk_begin(&ctx, "Control points", nk_rect(550, 20, 350, 300),
                     NK_WINDOW_BORDER | NK_WINDOW_TITLE)) {
            ui_header(&ctx, &media, "Glyphs");
            nk_layout_row_dynamic(&ctx, 30, 1);
            float glyphSize = cpPlot.glyphSize();
            nk_property_float(&ctx, "Size:", 1.0f, &glyphSize, 20.0f, 1.0f, 0.1f);
            if (glyphSize != cpPlot.glyphSize()) {
                cpPlot.setGlyphSize(glyphSize);
            }

            nk_layout_row_dynamic(&ctx, 30, 1);
            float opacity = 1.0f;
            nk_property_float(&ctx, "Opacity:", 0.0f, &opacity, 1.0f, 0.1f, 0.01f);
            // TODO

            nk_layout_row_dynamic(&ctx, 20, 1);
            nk_spacer(&ctx);

            ui_header(&ctx, &media, "Colors");
            nk_layout_row_begin(&ctx, NK_DYNAMIC, 30, 2);
            nk_layout_row_push(&ctx, 0.3f);
            nk_label(&ctx, "Scale:", NK_TEXT_RIGHT);
            nk_layout_row_push(&ctx, 0.7f);
            if (nk_combo_begin_label(&ctx, COLORMAP_NAMES[cpColorScaleType].c_str(),
                                     nk_vec2(nk_widget_width(&ctx), 200))) {
                nk_layout_row_dynamic(&ctx, 25, 1);
                size_t i = 0;
                for (auto it = COLORMAP_NAMES.begin();
                     it != COLORMAP_NAMES.end(); it++) {
                    if (nk_combo_item_label(&ctx, it->c_str(), NK_TEXT_LEFT)) {
                        enum Main::ColorScaleType type = static_cast<Main::ColorScaleType>(i);
                        if (type != cpColorScaleType) {
                            m.setCPColorScale(type);
                            cpColorScaleType = type;
                        }
                    }
                    i++;
                }
                nk_combo_end(&ctx);
            }
            nk_layout_row_end(&ctx);

            nk_layout_row_begin(&ctx, NK_DYNAMIC, 30, 2);
            nk_layout_row_push(&ctx, 0.3f);
            nk_label(&ctx, "Map to:", NK_TEXT_RIGHT);
            nk_layout_row_push(&ctx, 0.7f);
            if (nk_combo_begin_label(&ctx, METRIC_NAMES[0].c_str(),
                                     nk_vec2(nk_widget_width(&ctx), 200))) {
                nk_layout_row_dynamic(&ctx, 25, 1);
                size_t i = 0;
                for (auto it = METRIC_NAMES.begin();
                     it != METRIC_NAMES.end(); it++) {
                    if (nk_combo_item_label(&ctx, it->c_str(), NK_TEXT_LEFT)) {
                        // TODO
                    }
                    i++;
                }
                nk_combo_end(&ctx);
            }
            nk_layout_row_end(&ctx);

            nk_layout_row_dynamic(&ctx, 10, 1);
            nk_spacer(&ctx);
            nk_layout_row_dynamic(&ctx, 10, 1);
            nk_image(&ctx, cpColormap_img);
        }
        nk_end(&ctx);

        if (nk_begin(&ctx, "Regular points", nk_rect(550, 340, 350, 410),
                     NK_WINDOW_BORDER | NK_WINDOW_TITLE)) {
            ui_header(&ctx, &media, "Splat");
            nk_layout_row_dynamic(&ctx, 30, 1);
            float alpha = splat.alpha();
            nk_property_float(&ctx, "Blur (alpha):", 5.0f, &alpha, 50.0f, 1.0f, 0.1f);
            if (alpha != splat.alpha()) {
                splat.setAlpha(alpha);
            }

            nk_layout_row_dynamic(&ctx, 30, 1);
            float beta = splat.beta();
            nk_property_float(&ctx, "Radius (beta):", 5.0f, &beta, 50.0f, 1.0f, 0.1f);
            if (beta != splat.beta()) {
                splat.setBeta(beta);
            }

            nk_layout_row_dynamic(&ctx, 20, 1);
            nk_spacer(&ctx);

            ui_header(&ctx, &media, "Glyphs");
            nk_layout_row_dynamic(&ctx, 30, 1);
            float glyphSize = rpPlot.glyphSize();
            nk_property_float(&ctx, "Size:", 1.0f, &glyphSize, 20.0f, 1.0f, 0.1f);
            if (glyphSize != rpPlot.glyphSize()) {
                rpPlot.setGlyphSize(glyphSize);
            }

            nk_layout_row_dynamic(&ctx, 30, 1);
            float opacity = 1.0f;
            nk_property_float(&ctx, "Opacity:", 0.0f, &opacity, 1.0f, 0.1f, 0.01f);
            // TODO

            nk_layout_row_dynamic(&ctx, 20, 1);
            nk_spacer(&ctx);

            ui_header(&ctx, &media, "Colors");
            nk_layout_row_begin(&ctx, NK_DYNAMIC, 30, 2);
            nk_layout_row_push(&ctx, 0.3f);
            nk_label(&ctx, "Scale:", NK_TEXT_RIGHT);
            nk_layout_row_push(&ctx, 0.7f);
            if (nk_combo_begin_label(&ctx, COLORMAP_NAMES[rpColorScaleType].c_str(),
                                     nk_vec2(nk_widget_width(&ctx), 200))) {
                nk_layout_row_dynamic(&ctx, 25, 1);
                size_t i = 0;
                for (auto it = COLORMAP_NAMES.begin();
                     it != COLORMAP_NAMES.end(); it++) {
                    if (nk_combo_item_label(&ctx, it->c_str(), NK_TEXT_LEFT)) {
                        enum Main::ColorScaleType type = static_cast<Main::ColorScaleType>(i);
                        if (type != rpColorScaleType) {
                            m.setRPColorScale(type);
                            rpColorScaleType = type;
                        }
                    }
                    i++;
                }
                nk_combo_end(&ctx);
            }
            nk_layout_row_end(&ctx);

            nk_layout_row_begin(&ctx, NK_DYNAMIC, 30, 2);
            nk_layout_row_push(&ctx, 0.3f);
            nk_label(&ctx, "Map to:", NK_TEXT_RIGHT);
            nk_layout_row_push(&ctx, 0.7f);
            if (nk_combo_begin_label(&ctx, METRIC_NAMES[0].c_str(),
                                     nk_vec2(nk_widget_width(&ctx), 200))) {
                nk_layout_row_dynamic(&ctx, 25, 1);
                size_t i = 0;
                for (auto it = METRIC_NAMES.begin(); it != METRIC_NAMES.end(); it++) {
                    if (nk_combo_item_label(&ctx, it->c_str(), NK_TEXT_LEFT)) {
                        // TODO
                    }
                    i++;
                }
                nk_combo_end(&ctx);
            }
            nk_layout_row_end(&ctx);

            nk_layout_row_dynamic(&ctx, 10, 1);
            nk_spacer(&ctx);
            nk_layout_row_dynamic(&ctx, 10, 1);
            nk_image(&ctx, rpColormap_img);
        }
        nk_end(&ctx);

        struct nk_style *s = &ctx.style;
        nk_style_push_color(&ctx, &s->window.background, nk_rgba(0, 0, 0, 0));
        nk_style_push_style_item(&ctx, &s->window.fixed_background,
                                 nk_style_item_color(nk_rgba(0,0,0,0)));
        if (nk_begin(&ctx, "Scatterplot", nk_rect(20, 20, PLOT_WIDTH, PLOT_HEIGHT),
                     NK_WINDOW_NO_SCROLLBAR)) {
            // Render components to their textures
            splat.draw();
            rpPlot.draw();
            cpPlot.draw();

            nk_layout_space_begin(&ctx, NK_STATIC, PLOT_HEIGHT, 4);
            struct nk_rect region = nk_layout_space_bounds(&ctx);

            // Add white background rect and draw textures on top
            nk_layout_space_push(&ctx, region);
            struct nk_command_buffer *canvas = nk_window_get_canvas(&ctx);
            nk_fill_rect(canvas, region, 0.0f, nk_rgba(255, 255, 255, 255));

            // Rest uses custom region
            region.x = region.y = 0.0f;
            region.w = static_cast<float>(PLOT_WIDTH);
            region.h = static_cast<float>(PLOT_HEIGHT);
            nk_layout_space_push(&ctx, region);
            nk_image(&ctx, splat_img);
            nk_layout_space_push(&ctx, region);
            nk_image(&ctx, rpPlot_img);
            nk_layout_space_push(&ctx, region);
            nk_image(&ctx, cpPlot_img);
            nk_layout_space_end(&ctx);
        }
        nk_end(&ctx);
        nk_style_pop_color(&ctx);
        nk_style_pop_style_item(&ctx);

        if (nk_begin(&ctx, "Values", nk_rect(20, 40 + PLOT_HEIGHT, PLOT_WIDTH, 220),
                     NK_WINDOW_BORDER | NK_WINDOW_TITLE | NK_WINDOW_NO_SCROLLBAR)) {
            cpBarChart.draw();
            rpBarChart.draw();

            // nk_widget(&bounds, &ctx);
            ui_header(&ctx, &media, "Control points");
            nk_layout_row_dynamic(&ctx, cpBarChart.height(), 1);
            nk_image(&ctx, cpBarChart_img);

            ui_header(&ctx, &media, "Regular points");
            nk_layout_row_dynamic(&ctx, rpBarChart.height(), 1);
            nk_image(&ctx, rpBarChart_img);
        }
        nk_end(&ctx);

        glViewport(0, 0, display_width, display_height);
        // glDisable(GL_BLEND);
        glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        device_draw(&device, &ctx, width, height, scale, NK_ANTI_ALIASING_ON);

        glfwSwapBuffers(win);
        // nk_clear(&ctx);
    }

    nk_font_atlas_clear(&atlas);
    nk_free(&ctx);
    glfwTerminate();
    return 0;
}