diff options
-rw-r--r-- | lineplot.cpp | 382 | ||||
-rw-r--r-- | lineplot.h | 119 | ||||
-rw-r--r-- | main.cpp | 53 | ||||
-rw-r--r-- | main.h | 4 | ||||
-rw-r--r-- | main_view.qml | 627 |
5 files changed, 857 insertions, 328 deletions
diff --git a/lineplot.cpp b/lineplot.cpp index b4d7412..83f6e12 100644 --- a/lineplot.cpp +++ b/lineplot.cpp @@ -19,86 +19,124 @@ #include "geometry.h" #include "scatterplot.h" +static const int SAMPLES = 128; + LinePlot::LinePlot(QQuickItem *parent) : QQuickFramebufferObject(parent) , m_sx(0, 1, 0, 1) , m_sy(0, 1, 0, 1) - , m_xyChanged(false) + , m_linesChanged(false) , m_valuesChanged(false) , m_colorScaleChanged(false) + + // Q_PROPERTY's + , m_iterations(15) + , m_kernelSize(32) + , m_smoothingFactor(0.2) + , m_smoothingIterations(1) + + , m_blockEndpoints(true) + , m_endsIterations(0) + , m_endsKernelSize(32) + , m_endsSmoothingFactor(0.5) + + , m_edgeSampling(15) + , m_advectionSpeed(0.5) + , m_relaxation(0) , m_bundleGPU(true) { + setFlag(QQuickItem::ItemHasContents); setTextureFollowsItemSize(false); } -void LinePlot::setColorScale(const ColorScale *colorScale) +void LinePlot::setColorScale(const ColorScale *scale) { - m_colorScale = colorScale; - if (m_values.size() > 0) { - // FIXME - m_colorScaleChanged = true; - update(); - } + m_cmap.resize(SAMPLES * 3); + scale->sample(SAMPLES, m_cmap.begin()); + + setColorScaleChanged(true); + update(); } void LinePlot::bundle() { - Graph g(m_xy.n_rows); - PointSet points; + m_gdBundlePtr.reset(new GraphDrawing); + *m_gdBundlePtr.get() = *m_gdPtr.get(); - for (arma::uword i = 0; i < m_xy.n_rows; i++) { - const arma::rowvec &row = m_xy.row(i); - points.push_back(Point2d(row[0], row[1])); + CPUBundling bundling(std::min(width(), height())); + bundling.setInput(m_gdBundlePtr.get()); - if (i > 0) { - g(i - 1, i) = m_values[i]; - g(i, i - 1) = m_values[i]; - } - } + bundling.niter = m_iterations; + bundling.h = m_kernelSize; + bundling.lambda = m_smoothingFactor; + bundling.liter = m_smoothingIterations; - m_gdPtr.reset(new GraphDrawing); - m_gdPtr.get()->build(&g, &points); + bundling.block_endpoints = m_blockEndpoints; + bundling.niter_ms = m_endsIterations; + bundling.h_ms = m_endsKernelSize; + bundling.lambda_ends = m_endsSmoothingFactor; + + bundling.spl = m_edgeSampling; + bundling.eps = m_advectionSpeed; + // TODO: use m_relaxation as lerp param towards original (without bundling) - CPUBundling bundling(std::min(width(), height())); - bundling.setInput(m_gdPtr.get()); if (m_bundleGPU) { bundling.bundleGPU(); } else { bundling.bundleCPU(); } + + setLinesChanged(true); } -void LinePlot::setXY(const arma::mat &xy) +void LinePlot::setLines(const arma::uvec &indices, const arma::mat &Y) { - if (xy.n_cols != 2) { + if (indices.n_elem % 2 != 0 || Y.n_cols != 2) { return; } - m_xy = xy; - m_xyChanged = true; - emit xyChanged(m_xy); + m_lines = Y.rows(indices); + + // Build the line plot's internal representation: a graph where each + // endpoint of a line is a node, each line is a link... + Graph g(Y.n_rows); + PointSet points; + + m_sx.setRange(0, width()); + m_sy.setRange(0, height()); + for (arma::uword i = 0; i < Y.n_rows; i++) { + points.push_back(Point2d(m_sx(Y(i, 0)), m_sy(Y(i, 1)))); + } + + for (arma::uword k = 0; k < m_values.size(); k++) { + arma::uword i = indices(2*k + 0), + j = indices(2*k + 1); - // Build the line plot's internal representation: graph where each endpoint - // of a line is a node, each line is a link; and then bundle the graph's - // edges + g(i, j) = g(j, i) = m_values[k]; + } + + m_gdPtr.reset(new GraphDrawing); + m_gdPtr.get()->build(&g, &points); + + // ... then bundle the edges bundle(); + emit linesChanged(m_lines); update(); } void LinePlot::setValues(const arma::vec &values) { - if (m_xy.n_rows > 0 - && (values.n_elem > 0 && values.n_elem != m_xy.n_rows)) { + if (m_lines.n_rows > 0 + && (values.n_elem > 0 && values.n_elem != m_lines.n_rows)) { return; } m_values.resize(values.n_elem); - LinearScale<float> scale(values.min(), values.max(), 0, 1.0f); - std::transform(values.begin(), values.end(), m_values.begin(), scale); + std::copy(values.begin(), values.end(), m_values.begin()); emit valuesChanged(values); - m_valuesChanged = true; + setValuesChanged(true); update(); } @@ -111,6 +149,162 @@ void LinePlot::setScale(const LinearScale<float> &sx, update(); } +// Q_PROPERTY's +void LinePlot::setIterations(int iterations) { + if (m_iterations == iterations) { + return; + } + + m_iterations = iterations; + emit iterationsChanged(m_iterations); + + bundle(); + update(); +} + +void LinePlot::setKernelSize(float kernelSize) +{ + if (m_kernelSize == kernelSize) { + return; + } + + m_kernelSize = kernelSize; + emit kernelSizeChanged(m_kernelSize); + + bundle(); + update(); +} + +void LinePlot::setSmoothingFactor(float smoothingFactor) +{ + if (m_smoothingFactor == smoothingFactor) { + return; + } + + m_smoothingFactor = smoothingFactor; + emit smoothingFactorChanged(m_smoothingFactor); + + bundle(); + update(); +} + +void LinePlot::setSmoothingIterations(int smoothingIterations) +{ + if (m_smoothingIterations == smoothingIterations) { + return; + } + + m_smoothingIterations = smoothingIterations; + emit smoothingIterationsChanged(m_smoothingIterations); + + bundle(); + update(); +} + +void LinePlot::setBlockEndpoints(bool blockEndpoints) +{ + if (m_blockEndpoints == blockEndpoints) { + return; + } + + m_blockEndpoints = blockEndpoints; + emit blockEndpointsChanged(m_blockEndpoints); + + bundle(); + update(); +} + +void LinePlot::setEndsIterations(int endsIterations) +{ + if (m_endsIterations == endsIterations) { + return; + } + + m_endsIterations = endsIterations; + emit endsIterationsChanged(m_endsIterations); + + bundle(); + update(); +} + +void LinePlot::setEndsKernelSize(float endsKernelSize) +{ + if (m_endsKernelSize == endsKernelSize) { + return; + } + + m_endsKernelSize = endsKernelSize; + emit endsKernelSizeChanged(m_endsKernelSize); + + bundle(); + update(); +} + +void LinePlot::setEndsSmoothingFactor(float endsSmoothingFactor) +{ + if (m_endsSmoothingFactor == endsSmoothingFactor) { + return; + } + + m_endsSmoothingFactor = endsSmoothingFactor; + emit endsSmoothingFactorChanged(m_endsSmoothingFactor); + + bundle(); + update(); +} + +void LinePlot::setEdgeSampling(float edgeSampling) +{ + if (m_edgeSampling == edgeSampling) { + return; + } + + m_edgeSampling = edgeSampling; + emit edgeSamplingChanged(m_edgeSampling); + + bundle(); + update(); +} + +void LinePlot::setAdvectionSpeed(float advectionSpeed) +{ + if (m_advectionSpeed == advectionSpeed) { + return; + } + + m_advectionSpeed = advectionSpeed; + emit advectionSpeedChanged(m_advectionSpeed); + + bundle(); + update(); +} + +void LinePlot::setRelaxation(float relaxation) +{ + if (m_relaxation == relaxation) { + return; + } + + m_relaxation = relaxation; + emit relaxationChanged(m_relaxation); + + bundle(); + update(); +} + +void LinePlot::setBundleGPU(bool bundleGPU) +{ + if (m_bundleGPU == bundleGPU) { + return; + } + + m_bundleGPU = bundleGPU; + emit bundleGPUChanged(m_bundleGPU); + + bundle(); + update(); +} + // ---------------------------------------------------------------------------- class LinePlotRenderer @@ -133,23 +327,21 @@ private: void updatePoints(); void updateValues(); void updateColormap(); - void updateTransform(); void copyPolylines(const GraphDrawing *gd); QSize m_size; std::vector<float> m_points; - const std::vector<float> *m_values; + const std::vector<float> *m_values, *m_cmap; std::vector<int> m_offsets; - float m_alpha, m_beta; - GLfloat m_transform[4][4]; - LinearScale<float> m_sx, m_sy; QQuickWindow *m_window; // used to reset OpenGL state (as per docs) QOpenGLFunctions gl; QOpenGLShaderProgram *m_program; GLuint m_VBOs[2], m_colormapTex; QOpenGLVertexArrayObject m_VAO; + GLfloat m_transform[4][4]; + LinearScale<float> m_sx, m_sy; bool m_pointsChanged, m_valuesChanged, m_colormapChanged; }; @@ -159,13 +351,12 @@ QQuickFramebufferObject::Renderer *LinePlot::createRenderer() const } LinePlotRenderer::LinePlotRenderer() - : m_sx(0, 1, 0, 1) - , m_sy(0, 1, 0, 1) - , gl(QOpenGLContext::currentContext()) + : gl(QOpenGLContext::currentContext()) + , m_sx(0.0f, 1.0f, 0.0f, 1.0f) + , m_sy(0.0f, 1.0f, 0.0f, 1.0f) { std::fill(&m_transform[0][0], &m_transform[0][0] + 16, 0.0f); m_transform[3][3] = 1.0f; - setupShaders(); setupVAOs(); setupTextures(); @@ -236,12 +427,39 @@ void LinePlotRenderer::setupTextures() LinePlotRenderer::~LinePlotRenderer() { + gl.glDeleteBuffers(2, m_VBOs); + gl.glDeleteTextures(1, &m_colormapTex); + delete m_program; } QOpenGLFramebufferObject *LinePlotRenderer::createFramebufferObject(const QSize &size) { m_size = size; + GLfloat w = m_size.width(), h = m_size.height(); + + GLfloat rangeOffset = Scatterplot::PADDING / w; + m_sx.setDomain(0, w); + m_sx.setRange(rangeOffset, 1.0f - rangeOffset); + GLfloat sx = 2.0f * m_sx.slope(); + GLfloat tx = 2.0f * m_sx.offset() - 1.0f; + + rangeOffset = Scatterplot::PADDING / h; + m_sy.setDomain(0, h); + m_sy.setRange(1.0f - rangeOffset, rangeOffset); // inverted on purpose + GLfloat sy = 2.0f * m_sy.slope(); + GLfloat ty = 2.0f * m_sy.offset() - 1.0f; + + // The transform matrix should be this (but transposed -- column major): + // [ sx 0.0f 0.0f tx ] + // [ 0.0f sy 0.0f ty ] + // [ 0.0f 0.0f 0.0f 0.0f ] + // [ 0.0f 0.0f 0.0f 1.0f ] + m_transform[0][0] = sx; + m_transform[1][1] = sy; + m_transform[3][0] = tx; + m_transform[3][1] = ty; + return QQuickFramebufferObject::Renderer::createFramebufferObject(m_size); } @@ -262,20 +480,31 @@ void LinePlotRenderer::render() updateColormap(); } + if (m_offsets.size() < 2) { + // Nothing to draw + return; + } + m_program->bind(); - m_program->setUniformValue("transform", m_transform); gl.glActiveTexture(GL_TEXTURE0); gl.glBindTexture(GL_TEXTURE_2D, m_colormapTex); m_program->setUniformValue("colormap", 0); + m_program->setUniformValue("transform", m_transform); gl.glClearColor(0, 0, 0, 0); gl.glClear(GL_COLOR_BUFFER_BIT); m_VAO.bind(); - for (int i = 0; i < m_offsets.size() - 1; i++) { - gl.glDrawArrays(GL_LINES, m_offsets[i], m_offsets[i + 1]); + gl.glEnable(GL_LINE_SMOOTH); + gl.glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + gl.glEnable(GL_BLEND); + gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + for (int i = 1; i < m_offsets.size(); i++) { + gl.glDrawArrays(GL_LINE_STRIP, m_offsets[i - 1], m_offsets[i] - m_offsets[i - 1]); } + gl.glDisable(GL_LINE_SMOOTH); + gl.glDisable(GL_BLEND); m_VAO.release(); m_program->release(); @@ -290,22 +519,20 @@ void LinePlotRenderer::copyPolylines(const GraphDrawing *gd) int pointsNum = 0; m_offsets.clear(); + m_offsets.reserve(gd->draw_order.size() + 1); m_offsets.push_back(0); - for (std::pair<float, const GraphDrawing::Polyline *> p : gd->draw_order) { + for (auto &p : gd->draw_order) { pointsNum += p.second->size(); m_offsets.push_back(pointsNum); } - m_points.resize(2 * pointsNum); - arma::uword i = 0, k = 0; - for (std::pair<float, const GraphDrawing::Polyline *> p : gd->draw_order) { - for (arma::uword j = 0; j < p.second->size(); j++) { - m_points[i + j + 0] = (*p.second)[j].x; - m_points[i + j + 1] = (*p.second)[j].y; + m_points.clear(); + m_points.reserve(2 * pointsNum); + for (auto &p : gd->draw_order) { + for (auto &point : *p.second) { + m_points.push_back(point.x); + m_points.push_back(point.y); } - - i += p.second->size(); - k++; } } @@ -313,54 +540,29 @@ void LinePlotRenderer::synchronize(QQuickFramebufferObject *item) { LinePlot *plot = static_cast<LinePlot *>(item); - m_pointsChanged = plot->xyChanged(); + m_pointsChanged = plot->linesChanged(); m_valuesChanged = plot->valuesChanged(); m_colormapChanged = plot->colorScaleChanged(); - copyPolylines(plot->graphDrawing()); + if (m_pointsChanged) { + copyPolylines(plot->bundleGraphDrawing()); + } m_values = &(plot->values()); - m_sx = plot->scaleX(); - m_sy = plot->scaleY(); + m_cmap = &(plot->colorScale()); m_window = plot->window(); // Reset so that we have the correct values by the next synchronize() - plot->setXYChanged(false); + plot->setLinesChanged(false); plot->setValuesChanged(false); plot->setColorScaleChanged(false); } -void LinePlotRenderer::updateTransform() -{ - GLfloat w = m_size.width(), h = m_size.height(); - - GLfloat rangeOffset = Scatterplot::PADDING / w; - m_sx.setRange(rangeOffset, 1.0f - rangeOffset); - GLfloat sx = 2.0f * m_sx.slope(); - GLfloat tx = 2.0f * m_sx.offset() - 1.0f; - - rangeOffset = Scatterplot::PADDING / h; - m_sy.setRange(1.0f - rangeOffset, rangeOffset); - GLfloat sy = 2.0f * m_sy.slope(); - GLfloat ty = 2.0f * m_sy.offset() - 1.0f; - - // The transform matrix should be this (but transposed -- column major): - // [ sx 0.0f 0.0f -tx ] - // [ 0.0f sy 0.0f -ty ] - // [ 0.0f 0.0f 0.0f 0.0f ] - // [ 0.0f 0.0f 0.0f 1.0f ] - m_transform[0][0] = sx; - m_transform[1][1] = sy; - m_transform[3][0] = tx; - m_transform[3][1] = ty; -} - void LinePlotRenderer::updatePoints() { gl.glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[0]); gl.glBufferData(GL_ARRAY_BUFFER, m_points.size() * sizeof(float), m_points.data(), GL_DYNAMIC_DRAW); - updateTransform(); m_pointsChanged = false; } @@ -376,8 +578,8 @@ void LinePlotRenderer::updateValues() void LinePlotRenderer::updateColormap() { gl.glBindTexture(GL_TEXTURE_2D, m_colormapTex); - //gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_cmap->size() / 3, 1, 0, GL_RGB, - // GL_FLOAT, m_cmap->data()); + gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_cmap->size() / 3, 1, 0, GL_RGB, + GL_FLOAT, m_cmap->data()); m_colormapChanged = false; } @@ -18,27 +18,46 @@ class LinePlot : public QQuickFramebufferObject { Q_OBJECT + + // Main bundling + Q_PROPERTY(int iterations READ iterations WRITE setIterations NOTIFY iterationsChanged) + Q_PROPERTY(float kernelSize READ kernelSize WRITE setKernelSize NOTIFY kernelSizeChanged) + Q_PROPERTY(float smoothingFactor READ smoothingFactor WRITE setSmoothingFactor NOTIFY smoothingFactorChanged) + Q_PROPERTY(int smoothingIterations READ smoothingIterations WRITE setSmoothingIterations NOTIFY smoothingIterationsChanged) + + // Ends bundling + Q_PROPERTY(bool blockEndpoints READ blockEndpoints WRITE setBlockEndpoints NOTIFY blockEndpointsChanged) + Q_PROPERTY(int endsIterations READ endsIterations WRITE setEndsIterations NOTIFY endsIterationsChanged) + Q_PROPERTY(float endsKernelSize READ endsKernelSize WRITE setEndsKernelSize NOTIFY endsKernelSizeChanged) + Q_PROPERTY(float endsSmoothingFactor READ endsSmoothingFactor WRITE setEndsSmoothingFactor NOTIFY endsSmoothingFactorChanged) + + // General bundling options + Q_PROPERTY(float edgeSampling READ edgeSampling WRITE setEdgeSampling NOTIFY edgeSamplingChanged) + Q_PROPERTY(float advectionSpeed READ advectionSpeed WRITE setAdvectionSpeed NOTIFY advectionSpeedChanged) + Q_PROPERTY(float relaxation READ relaxation WRITE setRelaxation NOTIFY relaxationChanged) + Q_PROPERTY(bool bundleGPU READ bundleGPU WRITE setBundleGPU NOTIFY bundleGPUChanged) + public: static const int PADDING = 20; LinePlot(QQuickItem *parent = 0); - void setColorScale(const ColorScale *colorScale); - void setAutoScale(bool autoScale); + void setColorScale(const ColorScale *scale); - const GraphDrawing *graphDrawing() const { return m_gdPtr.get(); } - const std::vector<float> &values() const { return m_values; } + const GraphDrawing *bundleGraphDrawing() const { return m_gdBundlePtr.get(); } + const std::vector<float> &values() const { return m_values; } + const std::vector<float> &colorScale() const { return m_cmap; } LinearScale<float> scaleX() const { return m_sx; } LinearScale<float> scaleY() const { return m_sy; } Renderer *createRenderer() const; - bool xyChanged() const { return m_xyChanged; } - bool valuesChanged() const { return m_valuesChanged; } + bool linesChanged() const { return m_linesChanged; } + bool valuesChanged() const { return m_valuesChanged; } bool colorScaleChanged() const { return m_colorScaleChanged; } - void setXYChanged(bool xyChanged) { - m_xyChanged = xyChanged; + void setLinesChanged(bool linesChanged) { + m_linesChanged = linesChanged; } void setValuesChanged(bool valuesChanged) { m_valuesChanged = valuesChanged; @@ -47,15 +66,66 @@ public: m_colorScaleChanged = colorScaleChanged; } + // Q_PROPERTY's + int iterations() const { return m_iterations; } + float kernelSize() const { return m_kernelSize; } + float smoothingFactor() const { return m_smoothingFactor; } + int smoothingIterations() const { return m_smoothingIterations; } + + void setIterations(int iterations); + void setKernelSize(float kernelSize); + void setSmoothingFactor(float smoothingFactor); + void setSmoothingIterations(int smoothingIterations); + + bool blockEndpoints() const { return m_blockEndpoints; } + int endsIterations() const { return m_endsIterations; } + float endsKernelSize() const { return m_endsKernelSize; } + float endsSmoothingFactor() const { return m_endsSmoothingFactor; } + + void setBlockEndpoints(bool blockEndpoints); + void setEndsIterations(int endsIterations); + void setEndsKernelSize(float endsKernelSize); + void setEndsSmoothingFactor(float endsSmoothingFactor); + + float edgeSampling() const { return m_edgeSampling; } + float advectionSpeed() const { return m_advectionSpeed; } + float relaxation() const { return m_relaxation; } + bool bundleGPU() const { return m_bundleGPU; } + + void setEdgeSampling(float edgeSampling); + void setAdvectionSpeed(float advectionSpeed); + void setRelaxation(float relaxation); + void setBundleGPU(bool bundleGPU); + signals: - void xyChanged(const arma::mat &xy); + void linesChanged(const arma::mat &xy); void valuesChanged(const arma::vec &values) const; void scaleChanged(const LinearScale<float> &sx, const LinearScale<float> &sy) const; + // Q_PROPERTY's + void iterationsChanged(int iterations) const; + void kernelSizeChanged(float kernelSize) const; + void smoothingFactorChanged(float smoothingFactor) const; + void smoothingIterationsChanged(int smoothingIterations) const; + + void blockEndpointsChanged(bool blockEndpoints) const; + void endsIterationsChanged(int endsIterations) const; + void endsKernelSizeChanged(float endsKernelSize) const; + void endsSmoothingFactorChanged(float endsSmoothingFactor) const; + + void edgeSamplingChanged(float edgeSampling) const; + void advectionSpeedChanged(float advectionSpeed) const; + void relaxationChanged(float relaxation) const; + void bundleGPUChanged(bool bundleGPU) const; + public slots: - void setXY(const arma::mat &xy); + // Lines are two consecutive elements in 'indices' (ref. points in 'Y') + void setLines(const arma::uvec &indices, const arma::mat &Y); + + // One value for each line void setValues(const arma::vec &values); + void setScale(const LinearScale<float> &sx, const LinearScale<float> &sy); @@ -63,21 +133,32 @@ private: void bundle(); // Data - arma::mat m_xy; + arma::mat m_lines; std::vector<float> m_values; // Visuals - const ColorScale *m_colorScale; - - void autoScale(); - bool m_autoScale; + std::vector<float> m_cmap; LinearScale<float> m_sx, m_sy; - - std::unique_ptr<GraphDrawing> m_gdPtr; + std::unique_ptr<GraphDrawing> m_gdPtr, m_gdBundlePtr; // Internal state - bool m_xyChanged, m_valuesChanged, m_colorScaleChanged; - bool m_bundleGPU; + bool m_linesChanged, m_valuesChanged, m_colorScaleChanged; + + // Q_PROPERTY's + int m_iterations; + float m_kernelSize; + float m_smoothingFactor; + int m_smoothingIterations; + + bool m_blockEndpoints; + int m_endsIterations; + float m_endsKernelSize; + float m_endsSmoothingFactor; + + float m_edgeSampling; + float m_advectionSpeed; + float m_relaxation; + bool m_bundleGPU; }; #endif // LINEPLOT_H @@ -153,9 +153,10 @@ int main(int argc, char **argv) // Initialize pointers to visual components m->cpPlot = engine.rootObjects()[0]->findChild<Scatterplot *>("cpPlot"); m->rpPlot = engine.rootObjects()[0]->findChild<Scatterplot *>("rpPlot"); + m->splat = engine.rootObjects()[0]->findChild<VoronoiSplat *>("splat"); + m->bundlePlot = engine.rootObjects()[0]->findChild<LinePlot *>("bundlePlot"); m->cpColormap = engine.rootObjects()[0]->findChild<Colormap *>("cpColormap"); m->rpColormap = engine.rootObjects()[0]->findChild<Colormap *>("rpColormap"); - m->splat = engine.rootObjects()[0]->findChild<VoronoiSplat *>("splat"); m->cpBarChart = engine.rootObjects()[0]->findChild<BarChart *>("cpBarChart"); m->rpBarChart = engine.rootObjects()[0]->findChild<BarChart *>("rpBarChart"); TransitionControl *plotTC = engine.rootObjects()[0]->findChild<TransitionControl *>("plotTC"); @@ -170,6 +171,20 @@ int main(int argc, char **argv) QObject::connect(m->cpPlot, &Scatterplot::xyInteractivelyChanged, m, &Main::setCP); + // Keep both scatterplots, the splat and line plot scaled equally and + // relative to the full plot + MapScaleHandler mapScaleHandler; + QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged, + m->cpPlot, &Scatterplot::setScale); + QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged, + m->rpPlot, &Scatterplot::setScale); + QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged, + m->splat, &VoronoiSplat::setScale); + QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged, + m->bundlePlot, &LinePlot::setScale); + QObject::connect(m->projectionHistory, &ProjectionHistory::currentMapChanged, + &mapScaleHandler, &MapScaleHandler::scaleToMap); + // Update projection as the cp are modified (either directly in the // manipulationHandler object or interactively in cpPlot ManipulationHandler manipulationHandler(X, cpIndices); @@ -183,18 +198,32 @@ int main(int argc, char **argv) // ... and update visual components whenever the history changes QObject::connect(m->projectionHistory, &ProjectionHistory::currentMapChanged, m, &Main::updateMap); - - // Keep both scatterplots and the splat scaled equally and relative to the - // full plot - MapScaleHandler mapScaleHandler; - QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged, - m->cpPlot, &Scatterplot::setScale); - QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged, - m->rpPlot, &Scatterplot::setScale); - QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged, - m->splat, &VoronoiSplat::setScale); QObject::connect(m->projectionHistory, &ProjectionHistory::currentMapChanged, - &mapScaleHandler, &MapScaleHandler::scaleToMap); + [m](const arma::mat &Y) { + // ... and bundling + const arma::mat &unreliability = m->projectionHistory->unreliability(); + arma::uvec indicesLargest = arma::sort_index(unreliability, "descending"); + auto numLargest = Y.n_rows * 0.1f; + indicesLargest = indicesLargest.subvec(0, numLargest-1); + m->bundlePlot->setValues(unreliability(indicesLargest)); + + const arma::uvec &cpIndices = m->projectionHistory->cpIndices(); + arma::uvec CPs = cpIndices(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, Y); + }); // Linking between selections SelectionHandler cpSelectionHandler(cpIndices.n_elem); @@ -12,6 +12,7 @@ #include "numericrange.h" #include "barchart.h" #include "colormap.h" +#include "lineplot.h" #include "scatterplot.h" #include "voronoisplat.h" @@ -96,6 +97,7 @@ public: cpPlot->setColorScale(colorScaleCPs.get()); cpBarChart->setColorScale(colorScaleCPs.get()); cpColormap->setColorScale(colorScaleCPs.get()); + bundlePlot->setColorScale(colorScaleCPs.get()); } Q_INVOKABLE void setRPColorScale(ColorScaleType colorScaleType) { @@ -122,6 +124,7 @@ public: Colormap *cpColormap, *rpColormap; Scatterplot *cpPlot, *rpPlot; VoronoiSplat *splat; + LinePlot *bundlePlot; // Color scales in use std::unique_ptr<ColorScale> colorScaleCPs, colorScaleRPs; @@ -188,6 +191,7 @@ private: , cpPlot(0) , rpPlot(0) , splat(0) + , bundlePlot(0) , projectionHistory(0) { } diff --git a/main_view.qml b/main_view.qml index bce7005..60a85aa 100644 --- a/main_view.qml +++ b/main_view.qml @@ -14,9 +14,6 @@ ApplicationWindow { Component.onCompleted: { setX(Screen.width / 2 - width / 2); setY(Screen.height / 2 - height / 2); - - this.minimumWidth = width; - this.minimumHeight = height; } menuBar: MenuBar { @@ -38,6 +35,11 @@ ApplicationWindow { MenuItem { action: selectRPsAction } MenuItem { action: selectCPsAction } } + + Menu { + title: "View" + MenuItem { action: toggleOptionsAction } + } } statusBar: StatusBar { @@ -78,28 +80,28 @@ ApplicationWindow { anchors.fill: parent } - Scatterplot { - id: rpPlot - objectName: "rpPlot" + LinePlot { + id: bundlePlot + objectName: "bundlePlot" x: parent.x y: parent.y z: 1 anchors.fill: parent - glyphSize: 3.0 } Scatterplot { - id: cpPlot - objectName: "cpPlot" + id: rpPlot + objectName: "rpPlot" x: parent.x y: parent.y z: 2 anchors.fill: parent + glyphSize: 3.0 } - LinePlot { - id: linePlot - objectName: "linePlot" + Scatterplot { + id: cpPlot + objectName: "cpPlot" x: parent.x y: parent.y z: 3 @@ -237,234 +239,434 @@ ApplicationWindow { } // Options panel - ColumnLayout { - Layout.alignment: Qt.AlignTop | Qt.AlignLeft - - GroupBox { - Layout.fillWidth: true - title: "Control points" - checkable: true - __checkbox.onClicked: { - cpPlot.visible = this.checked; - - if (this.checked) { - cpPlot.z = 0; - rpPlot.z = 0; - } else { - cpPlot.z = 0; - rpPlot.z = 1; - } - } - - ColumnLayout { - GroupBox { - flat: true - title: "Colors" - - GridLayout { - columns: 2 + RowLayout { + id: optionsPanel - Label { text: "Map to:" } - ComboBox { - id: cpPlotMetricComboBox - model: metricsModel - } + ColumnLayout { + Layout.alignment: Qt.AlignTop | Qt.AlignLeft - Label { text: "Color map:" } - ComboBox { - id: cpPlotColormapCombo - model: colormapModel - onActivated: - Main.setCPColorScale(model.get(index).value); - } + GroupBox { + Layout.fillWidth: true + title: "Control points" + checkable: true + __checkbox.onClicked: { + cpPlot.visible = this.checked; + + if (this.checked) { + cpPlot.z = 0; + rpPlot.z = 0; + } else { + cpPlot.z = 0; + rpPlot.z = 1; } } - GroupBox { - flat: true - title: "Glyphs" - - GridLayout { - columns: 2 - - Label { text: "Size:" } - SpinBox { - id: cpGlyphSizeSpinBox - maximumValue: 100 - minimumValue: 6 - decimals: 1 - stepSize: 1 - value: cpPlot.glyphSize - onValueChanged: cpPlot.glyphSize = this.value + ColumnLayout { + GroupBox { + flat: true + title: "Colors" + + GridLayout { + columns: 2 + + Label { text: "Map to:" } + ComboBox { + id: cpPlotMetricComboBox + model: metricsModel + } + + Label { text: "Color map:" } + ComboBox { + id: cpPlotColormapCombo + model: colormapModel + onActivated: + Main.setCPColorScale(model.get(index).value); + } } + } - Label { text: "Opacity:" } - Slider { - id: cpPlotOpacitySlider - tickmarksEnabled: true - stepSize: 0.1 - maximumValue: 1 - minimumValue: 0 - value: cpPlot.opacity - onValueChanged: cpPlot.opacity = this.value + GroupBox { + flat: true + title: "Glyphs" + + GridLayout { + columns: 2 + + Label { text: "Size:" } + SpinBox { + id: cpGlyphSizeSpinBox + maximumValue: 100 + minimumValue: 6 + decimals: 1 + stepSize: 1 + value: cpPlot.glyphSize + onValueChanged: cpPlot.glyphSize = this.value + } + + Label { text: "Opacity:" } + Slider { + id: cpPlotOpacitySlider + tickmarksEnabled: true + stepSize: 0.1 + maximumValue: 1 + minimumValue: 0 + value: cpPlot.opacity + onValueChanged: cpPlot.opacity = this.value + } } } } } - } - - GroupBox { - Layout.fillWidth: true - title: "Regular points" - checked: true - checkable: true - __checkbox.onClicked: { - rpPlot.visible = this.checked; - splat.visible = this.checked; - } - - ColumnLayout { - GroupBox { - flat: true - title: "Colors" - - GridLayout { - columns: 2 - Label { text: "Map to:" } - ComboBox { - id: rpPlotMetricComboBox - model: metricsModel - } - - Label { text: "Color map:" } - ComboBox { - id: rpPlotColormapCombo - model: colormapModel - onActivated: - Main.setRPColorScale(model.get(index).value); - } - } + GroupBox { + Layout.fillWidth: true + title: "Regular points" + checked: true + checkable: true + __checkbox.onClicked: { + rpPlot.visible = this.checked; + splat.visible = this.checked; } - GroupBox { - flat: true - title: "Splat" - - GridLayout { - columns: 2 - - Label { text: "Blur (α):" } - SpinBox { - id: alphaSpinBox - maximumValue: 100 - minimumValue: 1 - value: splat.alpha - decimals: 2 - stepSize: 1 - onValueChanged: splat.alpha = this.value + ColumnLayout { + GroupBox { + flat: true + title: "Colors" + + GridLayout { + columns: 2 + + Label { text: "Map to:" } + ComboBox { + id: rpPlotMetricComboBox + model: metricsModel + } + + Label { text: "Color map:" } + ComboBox { + id: rpPlotColormapCombo + model: colormapModel + onActivated: + Main.setRPColorScale(model.get(index).value); + } } + } - Label { text: "Radius (β):" } - SpinBox { - id: betaSpinBox - maximumValue: 100 - minimumValue: 1 - value: splat.beta - decimals: 2 - stepSize: 1 - onValueChanged: splat.beta = this.value + GroupBox { + flat: true + title: "Splat" + + GridLayout { + columns: 2 + + Label { text: "Blur (α):" } + SpinBox { + id: alphaSpinBox + maximumValue: 100 + minimumValue: 1 + value: splat.alpha + decimals: 2 + stepSize: 1 + onValueChanged: splat.alpha = this.value + } + + Label { text: "Radius (β):" } + SpinBox { + id: betaSpinBox + maximumValue: 100 + minimumValue: 1 + value: splat.beta + decimals: 2 + stepSize: 1 + onValueChanged: splat.beta = this.value + } + + Label { text: "Opacity:" } + Slider { + id: splatOpacitySlider + tickmarksEnabled: true + stepSize: 0.1 + maximumValue: 1 + minimumValue: 0 + value: splat.opacity + onValueChanged: splat.opacity = this.value + } } + } - Label { text: "Opacity:" } - Slider { - id: splatOpacitySlider - tickmarksEnabled: true - stepSize: 0.1 - maximumValue: 1 - minimumValue: 0 - value: splat.opacity - onValueChanged: splat.opacity = this.value + GroupBox { + flat: true + title: "Glyphs" + + GridLayout { + columns: 2 + + Label { text: "Size:" } + SpinBox { + id: rpGlyphSizeSpinBox + maximumValue: 100 + minimumValue: 2 + decimals: 1 + stepSize: 1 + value: rpPlot.glyphSize + onValueChanged: rpPlot.glyphSize = this.value + } + + Label { text: "Opacity:" } + Slider { + id: rpPlotOpacitySlider + tickmarksEnabled: true + stepSize: 0.1 + maximumValue: 1 + minimumValue: 0 + value: rpPlot.opacity + onValueChanged: rpPlot.opacity = this.value + } } } } + } - GroupBox { - flat: true - title: "Glyphs" - - GridLayout { - columns: 2 - - Label { text: "Size:" } - SpinBox { - id: rpGlyphSizeSpinBox - maximumValue: 100 - minimumValue: 2 - decimals: 1 - stepSize: 1 - value: rpPlot.glyphSize - onValueChanged: rpPlot.glyphSize = this.value + GroupBox { + Layout.fillWidth: true + id: metricsGroupBox + title: "Projection metrics" + property RadioButton current: currentMetricRadioButton + + Column { + ExclusiveGroup { id: wrtMetricsGroup } + + RadioButton { + id: currentMetricRadioButton + text: "Current" + exclusiveGroup: wrtMetricsGroup + checked: true + onClicked: { + if (!Main.setObserverType(Main.ObserverCurrent)) { + metricsGroupBox.current.checked = true; + } else { + metricsGroupBox.current = this; + } } - - Label { text: "Opacity:" } - Slider { - id: rpPlotOpacitySlider - tickmarksEnabled: true - stepSize: 0.1 - maximumValue: 1 - minimumValue: 0 - value: rpPlot.opacity - onValueChanged: rpPlot.opacity = this.value + } + RadioButton { + id: diffPreviousMetricRadioButton + text: "Diff. to previous" + exclusiveGroup: wrtMetricsGroup + onClicked: { + if (!Main.setObserverType(Main.ObserverDiffPrevious)) { + metricsGroupBox.current.checked = true; + } else { + metricsGroupBox.current = this; + } + } + } + RadioButton { + id: diffFirstMetricRadioButton + text: "Diff. to first" + exclusiveGroup: wrtMetricsGroup + onClicked: { + if (!Main.setObserverType(Main.ObserverDiffFirst)) { + metricsGroupBox.current.checked = true; + } else { + metricsGroupBox.current = this; + } } } } } } - GroupBox { - Layout.fillWidth: true - id: metricsGroupBox - title: "Projection metrics" - property RadioButton current: currentMetricRadioButton - - Column { - ExclusiveGroup { id: wrtMetricsGroup } - - RadioButton { - id: currentMetricRadioButton - text: "Current" - exclusiveGroup: wrtMetricsGroup - checked: true - onClicked: { - if (!Main.setObserverType(Main.ObserverCurrent)) { - metricsGroupBox.current.checked = true; - } else { - metricsGroupBox.current = this; + ColumnLayout { + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + + GroupBox { + Layout.fillWidth: true + id: bundlingGroupBox + title: "Bundling" + checked: true + checkable: true + __checkbox.onClicked: { + bundlePlot.visible = this.checked; + } + + ColumnLayout { + GroupBox { + flat: true + title: "Main bundling" + + GridLayout { + columns: 2 + + Label { text: "Iterations:" } + SpinBox { + maximumValue: 100 + minimumValue: 0 + decimals: 0 + stepSize: 1 + value: bundlePlot.iterations + onValueChanged: bundlePlot.iterations = this.value + } + + Label { text: "Kernel size:" } + SpinBox { + maximumValue: 100 + minimumValue: 3 + decimals: 1 + stepSize: 1 + value: bundlePlot.kernelSize + onValueChanged: bundlePlot.kernelSize = this.value + } + + Label { text: "Smoothing factor:" } + Slider { + tickmarksEnabled: true + stepSize: 0.1 + maximumValue: 1 + minimumValue: 0 + value: bundlePlot.smoothingFactor + onValueChanged: bundlePlot.smoothingFactor = this.value + } + + Label { text: "Smoothing iterations:" } + SpinBox { + maximumValue: 100 + minimumValue: 0 + decimals: 0 + stepSize: 1 + value: bundlePlot.smoothingIterations + onValueChanged: bundlePlot.smoothingIterations = this.value + } } } - } - RadioButton { - id: diffPreviousMetricRadioButton - text: "Diff. to previous" - exclusiveGroup: wrtMetricsGroup - onClicked: { - if (!Main.setObserverType(Main.ObserverDiffPrevious)) { - metricsGroupBox.current.checked = true; - } else { - metricsGroupBox.current = this; + + GroupBox { + flat: true + title: "Ends bundling" + + GridLayout { + columns: 2 + + CheckBox { + Layout.columnSpan: 2 + text: "Block endpoints" + checked: bundlePlot.blockEndpoints + onClicked: bundlePlot.blockEndpoints = this.checked + } + + Label { text: "Iterations:" } + SpinBox { + maximumValue: 100 + minimumValue: 0 + decimals: 0 + stepSize: 1 + value: bundlePlot.endsIterations + onValueChanged: bundlePlot.endsIterations = this.value + } + + Label { text: "Kernel size:" } + SpinBox { + maximumValue: 100 + minimumValue: 3 + decimals: 1 + stepSize: 1 + value: bundlePlot.endsKernelSize + onValueChanged: bundlePlot.endsKernelSize = this.value + } + + Label { text: "Smoothing factor:" } + Slider { + tickmarksEnabled: true + stepSize: 0.1 + maximumValue: 1 + minimumValue: 0 + value: bundlePlot.endsSmoothingFactor + onValueChanged: bundlePlot.endsSmoothingFactor = this.value + } } } - } - RadioButton { - id: diffFirstMetricRadioButton - text: "Diff. to first" - exclusiveGroup: wrtMetricsGroup - onClicked: { - if (!Main.setObserverType(Main.ObserverDiffFirst)) { - metricsGroupBox.current.checked = true; - } else { - metricsGroupBox.current = this; + + GroupBox { + flat: true + title: "General" + + GridLayout { + columns: 2 + + Label { text: "Opacity:" } + Slider { + tickmarksEnabled: true + stepSize: 0.1 + maximumValue: 1 + minimumValue: 0 + value: bundlePlot.opacity + onValueChanged: bundlePlot.opacity = this.value + } + + Label { text: "Edge sampling:" } + SpinBox { + maximumValue: 100 + minimumValue: 3 + decimals: 1 + stepSize: 1 + value: bundlePlot.edgeSampling + onValueChanged: bundlePlot.edgeSampling = this.value + } + + Label { text: "Advection speed:" } + Slider { + tickmarksEnabled: true + stepSize: 0.1 + maximumValue: 1 + minimumValue: 0 + value: bundlePlot.advectionSpeed + onValueChanged: bundlePlot.advectionSpeed = this.value + } + + Label { text: "Density estimation:" } + RowLayout { + ExclusiveGroup { id: densityEstimationGroup } + RadioButton { + text: "Exact" + exclusiveGroup: densityEstimationGroup + } + RadioButton { + text: "Fast" + exclusiveGroup: densityEstimationGroup + checked: true + } + } + + Label { text: "Bundle shape:" } + RowLayout { + ExclusiveGroup { id: bundleShapeGroup } + RadioButton { + text: "FDEB" + exclusiveGroup: bundleShapeGroup + checked: true + } + RadioButton { + text: "HEB" + exclusiveGroup: bundleShapeGroup + } + } + + Label { text: "Relaxation:" } + Slider { + tickmarksEnabled: true + stepSize: 0.1 + maximumValue: 1 + minimumValue: 0 + value: bundlePlot.relaxation + onValueChanged: bundlePlot.relaxation = this.value + } + + CheckBox { + Layout.columnSpan: 2 + text: "Use GPU" + checked: bundlePlot.bundleGPU + onClicked: bundlePlot.bundleGPU = this.checked + } } } } @@ -562,6 +764,17 @@ ApplicationWindow { } Action { + id: toggleOptionsAction + text: "&Options" + shortcut: "Ctrl+O" + checkable: true + checked: true + onToggled: { + optionsPanel.visible = this.checked; + } + } + + Action { id: selectCPsAction text: "&Control points" shortcut: "C" |