diff options
-rw-r--r-- | colorscale.cpp | 12 | ||||
-rw-r--r-- | colorscale.h | 6 | ||||
-rw-r--r-- | continuouscolorscale.cpp | 556 | ||||
-rw-r--r-- | continuouscolorscale.h | 21 | ||||
-rw-r--r-- | dist.cpp | 7 | ||||
-rw-r--r-- | distortionmeasure.h | 12 | ||||
-rw-r--r-- | distortionobserver.cpp | 33 | ||||
-rw-r--r-- | distortionobserver.h | 29 | ||||
-rw-r--r-- | effectivenessobserver.cpp | 42 | ||||
-rw-r--r-- | effectivenessobserver.h | 26 | ||||
-rw-r--r-- | forceScheme.cpp | 10 | ||||
-rw-r--r-- | geometry.cpp | 39 | ||||
-rw-r--r-- | geometry.h | 13 | ||||
-rw-r--r-- | historygraph.cpp | 325 | ||||
-rw-r--r-- | historygraph.h | 38 | ||||
-rw-r--r-- | interactionhandler.cpp | 5 | ||||
-rw-r--r-- | interactionhandler.h | 6 | ||||
-rw-r--r-- | lamp.cpp | 3 | ||||
-rw-r--r-- | main.cpp | 89 | ||||
-rw-r--r-- | main_view.qml | 159 | ||||
-rw-r--r-- | measures.cpp | 76 | ||||
-rw-r--r-- | mp.h | 15 | ||||
-rw-r--r-- | npdistortion.cpp | 13 | ||||
-rw-r--r-- | npdistortion.h | 16 | ||||
-rw-r--r-- | pm.pro | 17 | ||||
-rw-r--r-- | scale.h | 81 | ||||
-rw-r--r-- | scatterplot.cpp | 343 | ||||
-rw-r--r-- | scatterplot.h | 40 | ||||
-rw-r--r-- | selectionhandler.cpp | 19 | ||||
-rw-r--r-- | selectionhandler.h | 24 | ||||
-rw-r--r-- | tsne.cpp | 15 |
31 files changed, 1866 insertions, 224 deletions
diff --git a/colorscale.cpp b/colorscale.cpp index 50fedae..85c036d 100644 --- a/colorscale.cpp +++ b/colorscale.cpp @@ -24,8 +24,9 @@ ColorScale::~ColorScale() void ColorScale::setExtents(qreal min, qreal max) { - if (min >= max) + if (min >= max) { return; + } m_min = min; m_max = max; @@ -48,23 +49,26 @@ static QColor lerp(const QColor &c1, const QColor &c2, qreal t) QColor ColorScale::color(qreal t) const { - if (t < m_min || t > m_max) + if (t < m_min || t > m_max) { return QColor(); + } // normalize t t = (t - m_min) / (m_max - m_min); // two colors, use a simpler solution - if (m_colors.size() == 2) + if (m_colors.size() == 2) { return lerp(m_colors.first(), m_colors.last(), t); + } // find which colors in the scale are adjacent to ours qreal step = 1.0 / m_colors.size(); int i = (int) (t / step); int j = i + 1; - if (i >= m_colors.size() - 1) + if (i >= m_colors.size() - 1) { return QColor(m_colors.last()); + } // normalize t between the two colors t = (t - i*step) / (j*step - i*step); diff --git a/colorscale.h b/colorscale.h index 6e1212d..6e65012 100644 --- a/colorscale.h +++ b/colorscale.h @@ -11,12 +11,12 @@ public: ColorScale(const QColor &firstColor, const QColor &lastColor); ColorScale(std::initializer_list<QColor> colors); ColorScale(const QList<QColor> &colors); - ~ColorScale(); + virtual ~ColorScale(); + virtual QColor color(qreal t) const; void setExtents(qreal min, qreal max); - QColor color(qreal t) const; -private: +protected: qreal m_min, m_max; QList<QColor> m_colors; }; diff --git a/continuouscolorscale.cpp b/continuouscolorscale.cpp new file mode 100644 index 0000000..f3dbc42 --- /dev/null +++ b/continuouscolorscale.cpp @@ -0,0 +1,556 @@ +#include "continuouscolorscale.h" + +#include <cmath> + +ContinuousColorScale::ContinuousColorScale(std::initializer_list<QColor> colors) + : ColorScale{colors} +{ +} + +QColor ContinuousColorScale::color(qreal t) const +{ + if (t < m_min || t > m_max) + return QColor(); + + // normalize t + t = (t - m_min) / (m_max - m_min); + + // find which colors in the scale are adjacent to ours + qreal step = 1.0 / m_colors.size(); + int i = (int) (t / step); + + if (i >= m_colors.size() - 1) { + return m_colors.last(); + } + + // normalize t between the two colors + int j = i + 1; + t = (t - i*step) / (j*step - i*step); + return m_colors[i + round(t)]; +} + +ContinuousColorScale ContinuousColorScale::builtin(BuiltinContinuousColorScale scale) +{ + switch (scale) { + case HEATED_OBJECTS: + return ContinuousColorScale{ + QColor( 0, 0, 0), + QColor( 35, 0, 0), + QColor( 52, 0, 0), + QColor( 60, 0, 0), + QColor( 63, 1, 0), + QColor( 64, 2, 0), + QColor( 68, 5, 0), + QColor( 69, 6, 0), + QColor( 72, 8, 0), + QColor( 74, 10, 0), + QColor( 77, 12, 0), + QColor( 78, 14, 0), + QColor( 81, 16, 0), + QColor( 83, 17, 0), + QColor( 85, 19, 0), + QColor( 86, 20, 0), + QColor( 89, 22, 0), + QColor( 91, 24, 0), + QColor( 92, 25, 0), + QColor( 94, 26, 0), + QColor( 95, 28, 0), + QColor( 98, 30, 0), + QColor(100, 31, 0), + QColor(102, 33, 0), + QColor(103, 34, 0), + QColor(105, 35, 0), + QColor(106, 36, 0), + QColor(108, 38, 0), + QColor(109, 39, 0), + QColor(111, 40, 0), + QColor(112, 42, 0), + QColor(114, 43, 0), + QColor(115, 44, 0), + QColor(117, 45, 0), + QColor(119, 47, 0), + QColor(119, 47, 0), + QColor(120, 48, 0), + QColor(122, 49, 0), + QColor(123, 51, 0), + QColor(125, 52, 0), + QColor(125, 52, 0), + QColor(126, 53, 0), + QColor(128, 54, 0), + QColor(129, 56, 0), + QColor(129, 56, 0), + QColor(131, 57, 0), + QColor(132, 58, 0), + QColor(134, 59, 0), + QColor(134, 59, 0), + QColor(136, 61, 0), + QColor(137, 62, 0), + QColor(137, 62, 0), + QColor(139, 63, 0), + QColor(139, 63, 0), + QColor(140, 65, 0), + QColor(142, 66, 0), + QColor(142, 66, 0), + QColor(143, 67, 0), + QColor(143, 67, 0), + QColor(145, 68, 0), + QColor(145, 68, 0), + QColor(146, 70, 0), + QColor(146, 70, 0), + QColor(148, 71, 0), + QColor(148, 71, 0), + QColor(149, 72, 0), + QColor(149, 72, 0), + QColor(151, 73, 0), + QColor(151, 73, 0), + QColor(153, 75, 0), + QColor(153, 75, 0), + QColor(154, 76, 0), + QColor(154, 76, 0), + QColor(154, 76, 0), + QColor(156, 77, 0), + QColor(156, 77, 0), + QColor(157, 79, 0), + QColor(157, 79, 0), + QColor(159, 80, 0), + QColor(159, 80, 0), + QColor(159, 80, 0), + QColor(160, 81, 0), + QColor(160, 81, 0), + QColor(162, 82, 0), + QColor(162, 82, 0), + QColor(163, 84, 0), + QColor(163, 84, 0), + QColor(165, 85, 0), + QColor(165, 85, 0), + QColor(166, 86, 0), + QColor(166, 86, 0), + QColor(166, 86, 0), + QColor(168, 87, 0), + QColor(168, 87, 0), + QColor(170, 89, 0), + QColor(170, 89, 0), + QColor(171, 90, 0), + QColor(171, 90, 0), + QColor(173, 91, 0), + QColor(173, 91, 0), + QColor(174, 93, 0), + QColor(174, 93, 0), + QColor(176, 94, 0), + QColor(176, 94, 0), + QColor(177, 95, 0), + QColor(177, 95, 0), + QColor(179, 96, 0), + QColor(179, 96, 0), + QColor(180, 98, 0), + QColor(182, 99, 0), + QColor(182, 99, 0), + QColor(183, 100, 0), + QColor(183, 100, 0), + QColor(185, 102, 0), + QColor(185, 102, 0), + QColor(187, 103, 0), + QColor(187, 103, 0), + QColor(188, 104, 0), + QColor(188, 104, 0), + QColor(190, 105, 0), + QColor(191, 107, 0), + QColor(191, 107, 0), + QColor(193, 108, 0), + QColor(193, 108, 0), + QColor(194, 109, 0), + QColor(196, 110, 0), + QColor(196, 110, 0), + QColor(197, 112, 0), + QColor(197, 112, 0), + QColor(199, 113, 0), + QColor(200, 114, 0), + QColor(200, 114, 0), + QColor(202, 116, 0), + QColor(202, 116, 0), + QColor(204, 117, 0), + QColor(205, 118, 0), + QColor(205, 118, 0), + QColor(207, 119, 0), + QColor(208, 121, 0), + QColor(208, 121, 0), + QColor(210, 122, 0), + QColor(211, 123, 0), + QColor(211, 123, 0), + QColor(213, 124, 0), + QColor(214, 126, 0), + QColor(214, 126, 0), + QColor(216, 127, 0), + QColor(217, 128, 0), + QColor(217, 128, 0), + QColor(219, 130, 0), + QColor(221, 131, 0), + QColor(221, 131, 0), + QColor(222, 132, 0), + QColor(224, 133, 0), + QColor(224, 133, 0), + QColor(225, 135, 0), + QColor(227, 136, 0), + QColor(227, 136, 0), + QColor(228, 137, 0), + QColor(230, 138, 0), + QColor(230, 138, 0), + QColor(231, 140, 0), + QColor(233, 141, 0), + QColor(233, 141, 0), + QColor(234, 142, 0), + QColor(236, 144, 0), + QColor(236, 144, 0), + QColor(238, 145, 0), + QColor(239, 146, 0), + QColor(241, 147, 0), + QColor(241, 147, 0), + QColor(242, 149, 0), + QColor(244, 150, 0), + QColor(244, 150, 0), + QColor(245, 151, 0), + QColor(247, 153, 0), + QColor(247, 153, 0), + QColor(248, 154, 0), + QColor(250, 155, 0), + QColor(251, 156, 0), + QColor(251, 156, 0), + QColor(253, 158, 0), + QColor(255, 159, 0), + QColor(255, 159, 0), + QColor(255, 160, 0), + QColor(255, 161, 0), + QColor(255, 163, 0), + QColor(255, 163, 0), + QColor(255, 164, 0), + QColor(255, 165, 0), + QColor(255, 167, 0), + QColor(255, 167, 0), + QColor(255, 168, 0), + QColor(255, 169, 0), + QColor(255, 169, 0), + QColor(255, 170, 0), + QColor(255, 172, 0), + QColor(255, 173, 0), + QColor(255, 173, 0), + QColor(255, 174, 0), + QColor(255, 175, 0), + QColor(255, 177, 0), + QColor(255, 178, 0), + QColor(255, 179, 0), + QColor(255, 181, 0), + QColor(255, 181, 0), + QColor(255, 182, 0), + QColor(255, 183, 0), + QColor(255, 184, 0), + QColor(255, 187, 7), + QColor(255, 188, 10), + QColor(255, 189, 14), + QColor(255, 191, 18), + QColor(255, 192, 21), + QColor(255, 193, 25), + QColor(255, 195, 29), + QColor(255, 197, 36), + QColor(255, 198, 40), + QColor(255, 200, 43), + QColor(255, 202, 51), + QColor(255, 204, 54), + QColor(255, 206, 61), + QColor(255, 207, 65), + QColor(255, 210, 72), + QColor(255, 211, 76), + QColor(255, 214, 83), + QColor(255, 216, 91), + QColor(255, 219, 98), + QColor(255, 221, 105), + QColor(255, 223, 109), + QColor(255, 225, 116), + QColor(255, 228, 123), + QColor(255, 232, 134), + QColor(255, 234, 142), + QColor(255, 237, 149), + QColor(255, 239, 156), + QColor(255, 240, 160), + QColor(255, 243, 167), + QColor(255, 246, 174), + QColor(255, 248, 182), + QColor(255, 249, 185), + QColor(255, 252, 193), + QColor(255, 253, 196), + QColor(255, 255, 204), + QColor(255, 255, 207), + QColor(255, 255, 211), + QColor(255, 255, 218), + QColor(255, 255, 222), + QColor(255, 255, 225), + QColor(255, 255, 229), + QColor(255, 255, 233), + QColor(255, 255, 236), + QColor(255, 255, 240), + QColor(255, 255, 244), + QColor(255, 255, 247), + QColor(255, 255, 255) + }; + case RED_GRAY_BLUE: + return ContinuousColorScale{ + QColor(221, 68, 68), + QColor(220, 69, 68), + QColor(220, 70, 69), + QColor(220, 71, 70), + QColor(220, 73, 71), + QColor(220, 74, 72), + QColor(220, 75, 73), + QColor(220, 77, 74), + QColor(220, 78, 75), + QColor(220, 79, 76), + QColor(220, 80, 76), + QColor(220, 81, 77), + QColor(220, 83, 78), + QColor(220, 84, 79), + QColor(220, 85, 80), + QColor(220, 86, 81), + QColor(220, 87, 82), + QColor(220, 88, 83), + QColor(220, 89, 84), + QColor(220, 91, 85), + QColor(220, 92, 85), + QColor(220, 93, 86), + QColor(220, 94, 87), + QColor(220, 95, 88), + QColor(220, 96, 89), + QColor(220, 97, 90), + QColor(220, 98, 91), + QColor(219, 99, 92), + QColor(219, 100, 93), + QColor(219, 101, 94), + QColor(219, 102, 95), + QColor(219, 103, 95), + QColor(219, 104, 96), + QColor(219, 105, 97), + QColor(219, 106, 98), + QColor(219, 107, 99), + QColor(219, 108, 100), + QColor(218, 109, 101), + QColor(218, 110, 102), + QColor(218, 111, 103), + QColor(218, 112, 104), + QColor(218, 113, 105), + QColor(218, 114, 106), + QColor(218, 115, 107), + QColor(218, 116, 108), + QColor(217, 117, 108), + QColor(217, 118, 109), + QColor(217, 119, 110), + QColor(217, 120, 111), + QColor(217, 121, 112), + QColor(217, 122, 113), + QColor(217, 123, 114), + QColor(216, 124, 115), + QColor(216, 125, 116), + QColor(216, 126, 117), + QColor(216, 127, 118), + QColor(216, 127, 119), + QColor(215, 128, 120), + QColor(215, 129, 121), + QColor(215, 130, 122), + QColor(215, 131, 123), + QColor(215, 132, 124), + QColor(214, 133, 125), + QColor(214, 134, 126), + QColor(214, 135, 127), + QColor(214, 136, 127), + QColor(214, 137, 128), + QColor(213, 138, 129), + QColor(213, 138, 130), + QColor(213, 139, 131), + QColor(213, 140, 132), + QColor(212, 141, 133), + QColor(212, 142, 134), + QColor(212, 143, 135), + QColor(212, 144, 136), + QColor(211, 145, 137), + QColor(211, 146, 138), + QColor(211, 146, 139), + QColor(211, 147, 140), + QColor(210, 148, 141), + QColor(210, 149, 142), + QColor(210, 150, 143), + QColor(209, 151, 144), + QColor(209, 152, 145), + QColor(209, 153, 146), + QColor(208, 153, 147), + QColor(208, 154, 148), + QColor(208, 155, 149), + QColor(207, 156, 150), + QColor(207, 157, 151), + QColor(207, 158, 152), + QColor(206, 159, 153), + QColor(206, 160, 154), + QColor(206, 160, 155), + QColor(205, 161, 156), + QColor(205, 162, 157), + QColor(205, 163, 158), + QColor(204, 164, 159), + QColor(204, 165, 160), + QColor(203, 166, 161), + QColor(203, 166, 162), + QColor(203, 167, 163), + QColor(202, 168, 164), + QColor(202, 169, 165), + QColor(201, 170, 166), + QColor(201, 171, 167), + QColor(201, 171, 168), + QColor(200, 172, 169), + QColor(200, 173, 170), + QColor(199, 174, 171), + QColor(199, 175, 172), + QColor(198, 176, 173), + QColor(198, 177, 174), + QColor(197, 177, 175), + QColor(197, 178, 176), + QColor(196, 179, 177), + QColor(196, 180, 178), + QColor(195, 181, 179), + QColor(195, 182, 180), + QColor(194, 182, 181), + QColor(194, 183, 182), + QColor(193, 184, 183), + QColor(193, 185, 184), + QColor(192, 186, 185), + QColor(192, 187, 186), + QColor(191, 187, 187), + QColor(190, 188, 188), + QColor(190, 189, 189), + QColor(189, 189, 190), + QColor(189, 188, 190), + QColor(188, 187, 190), + QColor(187, 186, 191), + QColor(187, 185, 191), + QColor(186, 184, 191), + QColor(185, 183, 191), + QColor(185, 182, 192), + QColor(184, 181, 192), + QColor(184, 180, 192), + QColor(183, 179, 193), + QColor(182, 178, 193), + QColor(182, 177, 193), + QColor(181, 176, 194), + QColor(180, 175, 194), + QColor(180, 174, 194), + QColor(179, 173, 194), + QColor(178, 172, 195), + QColor(178, 171, 195), + QColor(177, 170, 195), + QColor(176, 169, 196), + QColor(176, 168, 196), + QColor(175, 167, 196), + QColor(174, 166, 196), + QColor(174, 165, 197), + QColor(173, 164, 197), + QColor(172, 163, 197), + QColor(172, 162, 197), + QColor(171, 161, 198), + QColor(170, 160, 198), + QColor(170, 159, 198), + QColor(169, 158, 199), + QColor(168, 157, 199), + QColor(167, 156, 199), + QColor(167, 155, 199), + QColor(166, 155, 200), + QColor(165, 154, 200), + QColor(165, 153, 200), + QColor(164, 152, 200), + QColor(163, 151, 201), + QColor(162, 150, 201), + QColor(162, 149, 201), + QColor(161, 148, 201), + QColor(160, 147, 202), + QColor(159, 146, 202), + QColor(159, 145, 202), + QColor(158, 144, 202), + QColor(157, 143, 203), + QColor(156, 142, 203), + QColor(155, 141, 203), + QColor(155, 140, 203), + QColor(154, 139, 204), + QColor(153, 138, 204), + QColor(152, 137, 204), + QColor(152, 136, 204), + QColor(151, 135, 205), + QColor(150, 134, 205), + QColor(149, 133, 205), + QColor(148, 132, 205), + QColor(147, 131, 206), + QColor(147, 130, 206), + QColor(146, 129, 206), + QColor(145, 128, 206), + QColor(144, 128, 207), + QColor(143, 127, 207), + QColor(142, 126, 207), + QColor(142, 125, 207), + QColor(141, 124, 208), + QColor(140, 123, 208), + QColor(139, 122, 208), + QColor(138, 121, 208), + QColor(137, 120, 209), + QColor(136, 119, 209), + QColor(135, 118, 209), + QColor(134, 117, 209), + QColor(133, 116, 209), + QColor(133, 115, 210), + QColor(132, 114, 210), + QColor(131, 113, 210), + QColor(130, 112, 210), + QColor(129, 111, 211), + QColor(128, 110, 211), + QColor(127, 109, 211), + QColor(126, 109, 211), + QColor(125, 108, 212), + QColor(124, 107, 212), + QColor(123, 106, 212), + QColor(122, 105, 212), + QColor(121, 104, 212), + QColor(120, 103, 213), + QColor(119, 102, 213), + QColor(118, 101, 213), + QColor(117, 100, 213), + QColor(115, 99, 213), + QColor(114, 98, 214), + QColor(113, 97, 214), + QColor(112, 96, 214), + QColor(111, 95, 214), + QColor(110, 94, 215), + QColor(109, 94, 215), + QColor(108, 93, 215), + QColor(106, 92, 215), + QColor(105, 91, 215), + QColor(104, 90, 216), + QColor(103, 89, 216), + QColor(101, 88, 216), + QColor(100, 87, 216), + QColor(99, 86, 216), + QColor(98, 85, 217), + QColor(96, 84, 217), + QColor(95, 83, 217), + QColor(94, 82, 217), + QColor(92, 81, 217), + QColor(91, 80, 218), + QColor(89, 80, 218), + QColor(88, 79, 218), + QColor(86, 78, 218), + QColor(85, 77, 219), + QColor(83, 76, 219), + QColor(82, 75, 219), + QColor(80, 74, 219), + QColor(78, 73, 219), + QColor(77, 72, 220), + QColor(75, 71, 220), + QColor(73, 70, 220), + QColor(71, 69, 220), + QColor(69, 68, 220), + QColor(68, 68, 221) + }; + default: + return ContinuousColorScale::builtin(ContinuousColorScale::HEATED_OBJECTS); + } +} diff --git a/continuouscolorscale.h b/continuouscolorscale.h new file mode 100644 index 0000000..f734702 --- /dev/null +++ b/continuouscolorscale.h @@ -0,0 +1,21 @@ +#ifndef CONTINUOUSCOLORSCALE_H +#define CONTINUOUSCOLORSCALE_H + +#include "colorscale.h" + +class ContinuousColorScale : public ColorScale +{ +public: + ContinuousColorScale(std::initializer_list<QColor> colors); + + enum BuiltinContinuousColorScale { + HEATED_OBJECTS, + RED_GRAY_BLUE + }; + + QColor color(qreal t) const; + + static ContinuousColorScale builtin(enum BuiltinContinuousColorScale); +}; + +#endif // CONTINUOUSCOLORSCALE_H @@ -5,16 +5,17 @@ double mp::euclidean(const arma::rowvec &x1, const arma::rowvec &x2) return arma::norm(x1 - x2, 2); } -arma::mat mp::dist(const arma::mat &X, double (*distCalc)(const arma::rowvec &, const arma::rowvec &)) +arma::mat mp::dist(const arma::mat &X, mp::DistFunc dfunc) { arma::uword n = X.n_rows; arma::mat D(n, n, arma::fill::zeros); - for (arma::uword i = 0; i < n; i++) + for (arma::uword i = 0; i < n; i++) { for (arma::uword j = 0; j < i; j++) { - D(i, j) = distCalc(X.row(i), X.row(j)); + D(i, j) = dfunc(X.row(i), X.row(j)); D(j, i) = D(i, j); } + } return D; } diff --git a/distortionmeasure.h b/distortionmeasure.h new file mode 100644 index 0000000..523f62b --- /dev/null +++ b/distortionmeasure.h @@ -0,0 +1,12 @@ +#ifndef DISTORTIONMEASURE_H +#define DISTORTIONMEASURE_H + +#include <armadillo> + +class DistortionMeasure +{ +public: + virtual arma::vec measure(const arma::mat &distA, const arma::mat &distB) = 0; +}; + +#endif // DISTORTIONMEASURE_H diff --git a/distortionobserver.cpp b/distortionobserver.cpp new file mode 100644 index 0000000..29d7e6f --- /dev/null +++ b/distortionobserver.cpp @@ -0,0 +1,33 @@ +#include "distortionobserver.h" + +#include "mp.h" + +DistortionObserver::DistortionObserver(const arma::mat &X, + const arma::uvec &sampleIndices) + : m_X(X) + , m_sampleIndices(sampleIndices) + , m_distortionMeasure(0) +{ + m_distX = mp::dist(m_X); +} + +void DistortionObserver::setMeasure(DistortionMeasure *measure) +{ + m_distortionMeasure = measure; +} + +void DistortionObserver::setMap(const arma::mat &Y) +{ + if (!m_distortionMeasure) { + return; + } + + arma::vec measures = m_distortionMeasure->measure(m_distX, mp::dist(Y)); + + if (m_Y.n_elem != 0) { + emit mapChanged(measures - m_measures); + } else { + m_Y = Y; + m_measures = measures; + } +} diff --git a/distortionobserver.h b/distortionobserver.h new file mode 100644 index 0000000..101c53d --- /dev/null +++ b/distortionobserver.h @@ -0,0 +1,29 @@ +#ifndef DISTORTIONOBSERVER_H +#define DISTORTIONOBSERVER_H + +#include <QObject> +#include <armadillo> + +#include "distortionmeasure.h" + +class DistortionObserver : public QObject +{ + Q_OBJECT +public: + DistortionObserver(const arma::mat &X, const arma::uvec &sampleIndices); + void setMeasure(DistortionMeasure *measure); + +signals: + void mapChanged(const arma::vec &distortion); + +public slots: + void setMap(const arma::mat &Y); + +private: + arma::mat m_X, m_Y, m_distX; + arma::uvec m_sampleIndices; + DistortionMeasure *m_distortionMeasure; + arma::vec m_measures; +}; + +#endif // DISTORTIONOBSERVER_H diff --git a/effectivenessobserver.cpp b/effectivenessobserver.cpp new file mode 100644 index 0000000..305b3f8 --- /dev/null +++ b/effectivenessobserver.cpp @@ -0,0 +1,42 @@ +#include "effectivenessobserver.h" + +EffectiveInteractionEnforcer::EffectiveInteractionEnforcer(const arma::uvec &sampleIndices) + : m_sampleIndices(sampleIndices) + , m_effectiveness(arma::zeros<arma::vec>(sampleIndices.n_elem)) +{ +} + +void EffectiveInteractionEnforcer::setSelection(const arma::uvec &selection) +{ + m_selection = selection; +} + +void EffectiveInteractionEnforcer::setMeasureDifference(const arma::vec &measure) +{ + m_measure = measure; + + if (m_selection.n_elem == 0) { + return; + } + + arma::uvec selectionIndices(m_selection); + for (auto it = selectionIndices.begin(); it != selectionIndices.end(); it++) { + *it = m_sampleIndices[*it]; + } + + double diff = arma::mean(m_measure(selectionIndices)); + int effectiveness; + if (diff > 0) { + effectiveness = 1; + } else if (diff < 0) { + effectiveness = -1; + } else { + effectiveness = 0; + } + + for (auto it = m_selection.cbegin(); it != m_selection.cend(); it++) { + m_effectiveness[*it] = effectiveness; + } + + emit effectivenessChanged(m_effectiveness); +} diff --git a/effectivenessobserver.h b/effectivenessobserver.h new file mode 100644 index 0000000..8331916 --- /dev/null +++ b/effectivenessobserver.h @@ -0,0 +1,26 @@ +#ifndef EFFECTIVEINTERACTIONENFORCER_H +#define EFFECTIVEINTERACTIONENFORCER_H + +#include <QObject> +#include <armadillo> + +class EffectiveInteractionEnforcer : public QObject +{ + Q_OBJECT +public: + EffectiveInteractionEnforcer(const arma::uvec &sampleIndices); + +signals: + void effectivenessChanged(const arma::vec &effectiveness); + +public slots: + void setSelection(const arma::uvec &selection); + void setMeasureDifference(const arma::vec &measure); + +private: + arma::mat m_effectiveness; + arma::uvec m_sampleIndices, m_selection; + arma::vec m_measure; +}; + +#endif // EFFECTIVEINTERACTIONENFORCER_H diff --git a/forceScheme.cpp b/forceScheme.cpp index 0dc7e34..22ab293 100644 --- a/forceScheme.cpp +++ b/forceScheme.cpp @@ -1,7 +1,6 @@ #include "mp.h" #include <algorithm> -#include <vector> static const double EPSILON = 1e-3; @@ -15,8 +14,9 @@ arma::mat mp::forceScheme(const arma::mat &D, { arma::uword n = Y.n_rows; V i(n), j(n); - for (arma::uword k = 0; k < n; k++) + for (arma::uword k = 0; k < n; k++) { i[k] = j[k] = k; + } double prevDeltaSum = 1. / 0.; for (size_t iter = 0; iter < maxIter; iter++) { @@ -26,8 +26,9 @@ arma::mat mp::forceScheme(const arma::mat &D, for (V::iterator a = i.begin(); a != i.end(); a++) { arma::shuffle(j); for (V::iterator b = j.begin(); b != j.end(); b++) { - if (*a == *b) + if (*a == *b) { continue; + } arma::rowvec direction(Y.row(*b) - Y.row(*a)); double d2 = std::max(arma::norm(direction, 2), EPSILON); @@ -37,8 +38,9 @@ arma::mat mp::forceScheme(const arma::mat &D, } } - if (fabs(prevDeltaSum - deltaSum) < tol) + if (fabs(prevDeltaSum - deltaSum) < tol) { break; + } prevDeltaSum = deltaSum; } diff --git a/geometry.cpp b/geometry.cpp new file mode 100644 index 0000000..3a4a861 --- /dev/null +++ b/geometry.cpp @@ -0,0 +1,39 @@ +#include "geometry.h" + +static const float PI = 3.1415f; + +int calculateCircleVertexCount(float radius) +{ + // 10 * sqrt(r) \approx 2*pi / acos(1 - 1 / (4*r)) + return int(10.0 * sqrt(radius)); +} + +void updateCircleGeometry(QSGGeometry *geometry, float radius, float cx, float cy) +{ + int vertexCount = geometry->vertexCount(); + + float theta = 2 * PI / float(vertexCount); + float c = cosf(theta); + float s = sinf(theta); + float x = radius; + float y = 0; + + QSGGeometry::Point2D *vertexData = geometry->vertexDataAsPoint2D(); + for (int i = 0; i < vertexCount; i++) { + vertexData[i].set(x + cx, y + cy); + + float t = x; + x = c*x - s*y; + y = s*t + c*y; + } +} + +void updateRectGeometry(QSGGeometry *geometry, float x, float y, float w, float h) +{ + QSGGeometry::Point2D *vertexData = geometry->vertexDataAsPoint2D(); + + vertexData[0].set(x, y); + vertexData[1].set(x + w, y); + vertexData[2].set(x + w, y + h); + vertexData[3].set(x, y + h); +} diff --git a/geometry.h b/geometry.h new file mode 100644 index 0000000..b29e41f --- /dev/null +++ b/geometry.h @@ -0,0 +1,13 @@ +#ifndef GEOMETRY_H +#define GEOMETRY_H + +#include <QSGGeometry> + +// Circle +int calculateCircleVertexCount(float radius); +void updateCircleGeometry(QSGGeometry *geometry, float radius, float cx, float cy); + +// Rect +void updateRectGeometry(QSGGeometry *geometry, float x, float y, float w, float h); + +#endif // GEOMETRY_H diff --git a/historygraph.cpp b/historygraph.cpp new file mode 100644 index 0000000..0aeec20 --- /dev/null +++ b/historygraph.cpp @@ -0,0 +1,325 @@ +#include "historygraph.h" + +#include <algorithm> +#include <functional> +#include <numeric> +#include <utility> + +#include "geometry.h" +#include "scale.h" + +static const float ASPECT = 4.f / 3.f; +static const float MARGIN = 0.1f; +static const float PADDING = 0.05f; + +static const float GLYPH_SIZE = 4.0f; +static const float GLYPH_OPACITY = 0.6f; + +class HistoryGraph::HistoryItemNode +{ +public: + HistoryItemNode(const arma::mat &item); + ~HistoryItemNode(); + + void append(HistoryItemNode *node); + const QList<HistoryItemNode *> &children() const { return m_next; } + + const arma::mat &item() const; + int depth() const { return m_depth; } + int breadth() const { return m_breadth; } + const QRectF &rect() const { return m_rect; } + + void setRect(const QRectF &rect) { m_rect = rect; } + + void updateDepth(); + void updateBreadth(); + + static int treeBreadth(HistoryItemNode *node, int level); + +private: + arma::mat m_item; + HistoryItemNode *m_prev; + QList<HistoryItemNode *> m_next; + int m_depth, m_breadth; + QRectF m_rect; +}; + +HistoryGraph::HistoryItemNode::HistoryItemNode(const arma::mat &item) + : m_item(item) + , m_prev(0) + , m_depth(1) + , m_breadth(1) +{ +} + +HistoryGraph::HistoryItemNode::~HistoryItemNode() +{ + m_prev = 0; + + while (!m_next.isEmpty()) { + delete m_next.takeLast(); + } +} + +void HistoryGraph::HistoryItemNode::updateDepth() +{ + HistoryGraph::HistoryItemNode *node = *std::max_element( + m_next.cbegin(), m_next.cend(), + [](const HistoryGraph::HistoryItemNode *n1, const HistoryGraph::HistoryItemNode *n2) { + return n1->depth() < n2->depth(); + }); + + m_depth = 1 + node->depth(); + + if (m_prev) { + m_prev->updateDepth(); + } +} + +int HistoryGraph::HistoryItemNode::treeBreadth(HistoryGraph::HistoryItemNode *node, int level) { + if (!node || level < 1) { + return 0; + } + + if (level == 1) { + return 1; + } + + return std::accumulate(node->m_next.cbegin(), node->m_next.cend(), 0, [](int b, HistoryGraph::HistoryItemNode *n) { + n->m_breadth = treeBreadth(n, n->depth()); + return b + n->m_breadth; + }); +} + +void HistoryGraph::HistoryItemNode::updateBreadth() +{ + HistoryItemNode *node = this; + while (node->m_prev) { + node = node->m_prev; + } + + node->m_breadth = treeBreadth(node, node->m_depth); +} + +void HistoryGraph::HistoryItemNode::append(HistoryItemNode *node) +{ + if (!node) { + return; + } + + m_next.append(node); + if (node->depth() + 1 > m_depth) { + updateDepth(); + } + + updateBreadth(); + + node->m_prev = this; +} + +const arma::mat &HistoryGraph::HistoryItemNode::item() const +{ + return m_item; +} + +HistoryGraph::HistoryGraph(QQuickItem *parent) + : QQuickItem(parent) + , m_firstNode(0) + , m_currentNode(0) + , m_needsUpdate(false) +{ + setClip(true); + setFlag(QQuickItem::ItemHasContents); + setAcceptedMouseButtons(Qt::LeftButton); +} + +HistoryGraph::~HistoryGraph() +{ + delete m_firstNode; + m_firstNode = 0; +} + +void HistoryGraph::addHistoryItem(const arma::mat &item) +{ + HistoryItemNode *newNode = new HistoryItemNode(item); + if (m_currentNode) { + m_currentNode->append(newNode); + } else { + m_firstNode = newNode; + } + + m_currentNode = newNode; + m_needsUpdate = true; + update(); +} + +void HistoryGraph::addScatterplot(QSGNode *node, const HistoryGraph::HistoryItemNode *historyItemNode, float x, float y, float w, float h) +{ + const arma::mat &xy = historyItemNode->item(); + int vertexCount = calculateCircleVertexCount(GLYPH_SIZE / 2); + + LinearScale sx(xy.col(0).min(), xy.col(0).max(), x, x + w); + LinearScale sy(xy.col(1).min(), xy.col(1).max(), y + h, y); // reverse on purpose + + for (arma::uword i = 0; i < xy.n_rows; i++) { + const arma::rowvec &row = xy.row(i); + QSGGeometryNode *glyphNode = new QSGGeometryNode; + + QSGGeometry *glyphGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); + glyphGeometry->setDrawingMode(GL_POLYGON); + updateCircleGeometry(glyphGeometry, GLYPH_SIZE / 2 - 0.5, sx(row[0]), sy(row[1])); + glyphNode->setGeometry(glyphGeometry); + glyphNode->setFlag(QSGNode::OwnsGeometry); + + QSGFlatColorMaterial *material = new QSGFlatColorMaterial; + material->setColor(QColor()); + glyphNode->setMaterial(material); + glyphNode->setFlag(QSGNode::OwnsMaterial); + + // Place the glyph geometry node under an opacity node + QSGOpacityNode *glyphOpacityNode = new QSGOpacityNode; + glyphOpacityNode->setOpacity(GLYPH_OPACITY); + glyphOpacityNode->appendChildNode(glyphNode); + node->appendChildNode(glyphOpacityNode); + } +} + +QSGNode *HistoryGraph::createNodeTree() +{ + if (!m_firstNode) { + return 0; + } + + //int breadth = m_firstNode->breadth(); + //int depth = m_firstNode->depth(); + + QSGNode *sceneGraphRoot = new QSGNode; + HistoryItemNode *histNode = m_firstNode; + float margin = height()*MARGIN; + float padding = height()*PADDING; + float h = height() - 2.f*margin; + float w = ASPECT * h; + float x = margin; + + QMatrix4x4 mat; + do { + QSGOpacityNode *opacityNode = new QSGOpacityNode; + opacityNode->setOpacity(histNode == m_currentNode ? 1.0f : 0.4f); + + histNode->setRect(QRectF(x, margin, w, h)); + QSGGeometryNode *histItemGeomNode = new QSGGeometryNode; + QSGGeometry *histItemGeom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4); + updateRectGeometry(histItemGeom, x, margin, w, h); + histItemGeom->setDrawingMode(GL_LINE_LOOP); + histItemGeomNode->setGeometry(histItemGeom); + histItemGeomNode->setFlag(QSGNode::OwnsGeometry); + + QSGFlatColorMaterial *material = new QSGFlatColorMaterial; + material->setColor(QColor()); + histItemGeomNode->setMaterial(material); + histItemGeomNode->setFlag(QSGNode::OwnsMaterial); + + addScatterplot(histItemGeomNode, histNode, x + padding, margin + padding, w - 2*padding, h - 2*padding); + opacityNode->appendChildNode(histItemGeomNode); + sceneGraphRoot->appendChildNode(opacityNode); + + x += w + 2.f*margin; + + QList<HistoryItemNode *> children = histNode->children(); + if (children.isEmpty()) { + break; + } + + // FIXME: add children + histNode = children[0]; + } while (true); + + // setWidth(xPos + radius + margin); + + return sceneGraphRoot; +} + +void HistoryGraph::updateNodeTree(QSGNode *root) +{ + if (!m_firstNode) { + return; + } + + // FIXME: (really) lame update algorithm + + QSGNode *child = root->firstChild(); + if (child) { + root->removeChildNode(child); + delete child; + } + + root->appendChildNode(createNodeTree()); +} + +QSGNode *HistoryGraph::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) +{ + QSGNode *root = 0; + if (!oldNode) { + root = new QSGNode; + QSGNode *node = createNodeTree(); + if (node) { + root->appendChildNode(node); + } + } else { + root = oldNode; + } + + if (m_needsUpdate) { + m_needsUpdate = false; + updateNodeTree(root); + } + + return root; +} + +HistoryGraph::HistoryItemNode *HistoryGraph::nodeAt(const QPointF &pos) const +{ + return nodeAt(pos, m_firstNode); +} + +HistoryGraph::HistoryItemNode *HistoryGraph::nodeAt(const QPointF &pos, HistoryGraph::HistoryItemNode *node) const +{ + if (!node) { + return 0; + } + + const QRectF &rect = node->rect(); + + if (pos.x() < rect.x()) { + return 0; + } + + if (rect.contains(pos)) { + return node; + } + + QList<HistoryGraph::HistoryItemNode *> children = node->children(); + for (auto it = children.begin(); it != children.end(); it++) { + HistoryGraph::HistoryItemNode *tmp = nodeAt(pos, *it); + if (tmp) { + return tmp; + } + } + + return 0; +} + +void HistoryGraph::mousePressEvent(QMouseEvent *event) +{ + HistoryGraph::HistoryItemNode *node = nodeAt(event->localPos()); + if (!node || node == m_currentNode) { + event->ignore(); + return; + } + + m_currentNode = node; + m_needsUpdate = true; + update(); + + emit currentItemChanged(m_currentNode->item()); +} diff --git a/historygraph.h b/historygraph.h new file mode 100644 index 0000000..60cce94 --- /dev/null +++ b/historygraph.h @@ -0,0 +1,38 @@ +#ifndef HISTORYGRAPH_H +#define HISTORYGRAPH_H + +#include <QtQuick> +#include <armadillo> + +class HistoryGraph : public QQuickItem +{ + Q_OBJECT +public: + HistoryGraph(QQuickItem *parent = 0); + ~HistoryGraph(); + +signals: + void currentItemChanged(const arma::mat &item); + +public slots: + void addHistoryItem(const arma::mat &item); + +protected: + QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *); + void mousePressEvent(QMouseEvent *event); + +private: + class HistoryItemNode; + + HistoryItemNode *nodeAt(const QPointF &pos) const; + HistoryItemNode *nodeAt(const QPointF &pos, HistoryItemNode *node) const; + + QSGNode *createNodeTree(); + void updateNodeTree(QSGNode *root); + void addScatterplot(QSGNode *node, const HistoryItemNode *historyItemNode, float x, float y, float w, float h); + + HistoryItemNode *m_firstNode, *m_currentNode; + bool m_needsUpdate; +}; + +#endif // HISTORYGRAPH_H diff --git a/interactionhandler.cpp b/interactionhandler.cpp index 50d0653..b79fdd8 100644 --- a/interactionhandler.cpp +++ b/interactionhandler.cpp @@ -3,15 +3,12 @@ #include "mp.h" InteractionHandler::InteractionHandler(const arma::mat &X, - const arma::vec &labels, const arma::uvec &sampleIndices) : m_X(X) - , m_Y(X.n_rows, 3) - , m_labels(labels) + , m_Y(X.n_rows, 2) , m_sampleIndices(sampleIndices) , m_technique(TECHNIQUE_LAMP) { - m_Y.col(2) = m_labels; } void InteractionHandler::setSubsample(const arma::mat &Ys) diff --git a/interactionhandler.h b/interactionhandler.h index 5468dab..0104d65 100644 --- a/interactionhandler.h +++ b/interactionhandler.h @@ -1,10 +1,9 @@ #ifndef INTERACTIONHANDLER_H #define INTERACTIONHANDLER_H +#include <QObject> #include <armadillo> -#include "scatterplot.h" - class InteractionHandler : public QObject { Q_OBJECT @@ -15,7 +14,7 @@ public: TECHNIQUE_LSP }; - InteractionHandler(const arma::mat &X, const arma::vec &labels, const arma::uvec &sampleIndices); + InteractionHandler(const arma::mat &X, const arma::uvec &sampleIndices); signals: void subsampleChanged(const arma::mat &Y); @@ -25,7 +24,6 @@ public slots: private: arma::mat m_X, m_Y; - arma::vec m_labels; arma::uvec m_sampleIndices; InteractiveTechnique m_technique; }; @@ -16,7 +16,7 @@ void mp::lamp(const arma::mat &X, const arma::uvec &sampleIndices, const arma::m const arma::mat &Xs = X.rows(sampleIndices); arma::uword sampleSize = sampleIndices.n_elem; - #pragma omp parallel for shared(X, Xs, sampleIndices, Ys, Y) + #pragma omp parallel for shared(X, Xs, Ys, Y) for (arma::uword i = 0; i < X.n_rows; i++) { const arma::rowvec &point = X.row(i); @@ -52,7 +52,6 @@ void mp::lamp(const arma::mat &X, const arma::uvec &sampleIndices, const arma::m arma::svd(U, s, V, At * B); arma::mat M = U.cols(0, 1) * V.t(); - // the projection of point i Y(i, arma::span(0, 1)) = (point - Xtil) * M + Ytil; } } @@ -6,14 +6,21 @@ #include <QQmlApplicationEngine> #include "mp.h" +#include "continuouscolorscale.h" #include "scatterplot.h" +#include "historygraph.h" #include "interactionhandler.h" +#include "selectionhandler.h" +#include "effectivenessobserver.h" +#include "distortionobserver.h" +#include "npdistortion.h" int main(int argc, char **argv) { QApplication app(argc, argv); qmlRegisterType<Scatterplot>("PM", 1, 0, "Scatterplot"); + qmlRegisterType<HistoryGraph>("PM", 1, 0, "HistoryGraph"); // Set up multisampling QSurfaceFormat fmt; @@ -22,10 +29,11 @@ int main(int argc, char **argv) QQmlApplicationEngine engine(QUrl("qrc:///main_view.qml")); arma::mat dataset; - if (argc > 1) + if (argc > 1) { dataset.load(argv[1], arma::raw_ascii); - else + } else { dataset.load(std::cin, arma::raw_ascii); + } arma::mat X = dataset.cols(0, dataset.n_cols - 2); arma::vec labels = dataset.col(dataset.n_cols - 1); @@ -33,24 +41,79 @@ int main(int argc, char **argv) arma::uword n = dataset.n_rows; arma::uword subsampleSize = (arma::uword) sqrt(n) * 3; arma::uvec sampleIndices = arma::randi<arma::uvec>(subsampleSize, arma::distr_param(0, n-1)); - arma::mat Ys = arma::randn(subsampleSize, 2); + arma::mat Ys(subsampleSize, 2, arma::fill::randn); Ys = mp::forceScheme(mp::dist(X.rows(sampleIndices)), Ys); - arma::mat subsampleData(subsampleSize, 3); - subsampleData.cols(0, 1) = Ys; - subsampleData.col(2) = labels(sampleIndices); + ColorScale colorScale{ + QColor("#1f77b4"), + QColor("#ff7f0e"), + QColor("#2ca02c"), + QColor("#d62728"), + QColor("#9467bd"), + QColor("#8c564b"), + QColor("#e377c2"), + QColor("#17becf"), + QColor("#7f7f7f"), + }; + colorScale.setExtents(labels.min(), labels.max()); + + //ContinuousColorScale colorScale = ContinuousColorScale::builtin(ContinuousColorScale::RED_GRAY_BLUE); + //colorScale.setExtents(-1, 1); Scatterplot *subsamplePlot = engine.rootObjects()[0]->findChild<Scatterplot *>("subsamplePlot"); + HistoryGraph *history = engine.rootObjects()[0]->findChild<HistoryGraph *>("history"); subsamplePlot->setAcceptedMouseButtons(Qt::LeftButton | Qt::MiddleButton | Qt::RightButton); - subsamplePlot->setData(subsampleData); + // subsamplePlot->setColorData(arma::zeros<arma::vec>(subsampleSize)); + subsamplePlot->setColorScale(&colorScale); Scatterplot *plot = engine.rootObjects()[0]->findChild<Scatterplot *>("plot"); // connect both plots through interaction handler - std::unique_ptr<InteractionHandler> interactionHandler(new InteractionHandler(X, labels, sampleIndices)); - QObject::connect(subsamplePlot, SIGNAL(dataChanged(const arma::mat &)), - interactionHandler.get(), SLOT(setSubsample(const arma::mat &))); - QObject::connect(interactionHandler.get(), SIGNAL(subsampleChanged(const arma::mat &)), - plot, SLOT(setData(const arma::mat &))); - interactionHandler.get()->setSubsample(Ys); + InteractionHandler interactionHandler(X, sampleIndices); + QObject::connect(subsamplePlot, SIGNAL(xyChanged(const arma::mat &)), + &interactionHandler, SLOT(setSubsample(const arma::mat &))); + QObject::connect(subsamplePlot, SIGNAL(xyInteractivelyChanged(const arma::mat &)), + &interactionHandler, SLOT(setSubsample(const arma::mat &))); + QObject::connect(&interactionHandler, SIGNAL(subsampleChanged(const arma::mat &)), + plot, SLOT(setXY(const arma::mat &))); + QObject::connect(subsamplePlot, SIGNAL(xyInteractivelyChanged(const arma::mat &)), + history, SLOT(addHistoryItem(const arma::mat &))); + QObject::connect(history, SIGNAL(currentItemChanged(const arma::mat &)), + subsamplePlot, SLOT(setXY(const arma::mat &))); + + SelectionHandler selectionHandler(sampleIndices); + QObject::connect(subsamplePlot, SIGNAL(selectionChanged(const QSet<int> &)), + &selectionHandler, SLOT(setSelection(const QSet<int> &))); + QObject::connect(&selectionHandler, SIGNAL(selectionChanged(const QSet<int> &)), + plot, SLOT(setSelection(const QSet<int> &))); + + /* + DistortionObserver distortionObs(X, sampleIndices); + std::unique_ptr<DistortionMeasure> distortionMeasure(new NPDistortion()); + distortionObs.setMeasure(distortionMeasure.get()); + QObject::connect(&interactionHandler, SIGNAL(subsampleChanged(const arma::mat &)), + &distortionObs, SLOT(setMap(const arma::mat &))); + QObject::connect(&distortionObs, SIGNAL(mapChanged(const arma::vec &)), + plot, SLOT(setColorData(const arma::vec &))); + + EffectiveInteractionEnforcer enforcer(sampleIndices); + QObject::connect(subsamplePlot, SIGNAL(selectionChanged(const arma::uvec &)), + &enforcer, SLOT(setSelection(const arma::uvec &))); + QObject::connect(plot, SIGNAL(colorDataChanged(const arma::vec &)), + &enforcer, SLOT(setMeasureDifference(const arma::vec &))); + QObject::connect(&enforcer, SIGNAL(effectivenessChanged(const arma::vec &)), + subsamplePlot, SLOT(setColorData(const arma::vec &))); + */ + + /* + ContinuousColorScale ccolorScale = ContinuousColorScale::builtin(ContinuousColorScale::RED_GRAY_BLUE); + ccolorScale.setExtents(-1, 1); + plot->setColorScale(&ccolorScale); + */ + plot->setColorScale(&colorScale); + plot->setColorData(labels); + + history->addHistoryItem(Ys); + subsamplePlot->setXY(Ys); + subsamplePlot->setColorData(labels(sampleIndices)); return app.exec(); } diff --git a/main_view.qml b/main_view.qml index 5afe270..49448cb 100644 --- a/main_view.qml +++ b/main_view.qml @@ -1,12 +1,14 @@ -import QtQuick 2.0 +import QtQuick 2.5 import QtQuick.Controls 1.3 import QtQuick.Dialogs 1.2 +import QtQuick.Extras 1.4 +import QtQuick.Layouts 1.2 import PM 1.0 ApplicationWindow { title: "Projection Manipulation" visible: true - width: 1200 + width: 900 height: 600 menuBar: MenuBar { @@ -14,54 +16,100 @@ ApplicationWindow { title: "File" MenuItem { action: openAction } MenuItem { action: savePlotAction } - MenuItem { action: saveDataAction } MenuItem { action: quitAction } } + + Menu { + title: "View" + MenuItem { + action: noneColorAction + exclusiveGroup: coloringGroup + } + MenuItem { + action: npColorAction + exclusiveGroup: coloringGroup + } + MenuItem { + action: stressColorAction + exclusiveGroup: coloringGroup + } + MenuItem { + action: silhouetteColorAction + exclusiveGroup: coloringGroup + } + } } - Item { + ColumnLayout { + spacing: 10 anchors.fill: parent + anchors.margins: this.spacing - Rectangle { - anchors.fill: subsamplePlot - border.width: 1 - border.color: "#cccccc" - z: 0 + RowLayout { + spacing: 10 + Layout.fillWidth: true + Layout.fillHeight: true + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + border.width: 1 + border.color: "#cccccc" + + Scatterplot { + id: subsamplePlot + objectName: "subsamplePlot" + anchors.fill: parent + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + border.width: 1 + border.color: "#cccccc" + + Scatterplot { + id: plot + objectName: "plot" + anchors.fill: parent + } + } } Rectangle { - anchors.fill: plot + Layout.fillWidth: true + Layout.minimumHeight: 150 border.width: 1 border.color: "#cccccc" - z: 0 - } - Scatterplot { - id: subsamplePlot - objectName: "subsamplePlot" - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - width: 0.5 * parent.width - z: 1 + HistoryGraph { + id: history + objectName: "history" + anchors.fill: parent + } } + } + + FileDialog { + id: fileOpenDialog + title: "Choose a data set to load..." + selectMultiple: false + selectExisting: true - Scatterplot { - id: plot - objectName: "plot" - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - width: 0.5 * parent.width - z: 1 + onAccepted: { + console.log("Loading data set: " + this.fileUrl) } } FileDialog { - id: fileDialog - title: "Choose a file..." + id: fileSaveDialog + title: "Save subsample mapping..." + selectMultiple: false + selectExisting: false + onAccepted: { - // datasetLoader.load(fileDialog.fileUrls) + subsamplePlot.saveToFile(this.fileUrl) } } @@ -76,20 +124,53 @@ ApplicationWindow { id: openAction text: "&Open..." shortcut: "Ctrl+O" - onTriggered: fileDialog.open() + onTriggered: fileOpenDialog.open() } Action { id: savePlotAction - text: "Save &plot" + text: "&Save subsample" shortcut: "Ctrl+S" - onTriggered: console.log("Save plot") + onTriggered: { + console.log("Saving subsample mapping...") + fileSaveDialog.open() + } } - Action { - id: saveDataAction - text: "Save &data" - shortcut: "Ctrl+D" - onTriggered: console.log("Save data") + ExclusiveGroup { + id: coloringGroup + + Action { + id: noneColorAction + text: "None" + shortcut: "Shift+O" + checked: true + checkable: true + onTriggered: console.log("None") + } + + Action { + id: npColorAction + text: "Neighborhood Preservation" + shortcut: "Shift+N" + checkable: true + onTriggered: console.log("NP") + } + + Action { + id: stressColorAction + text: "Stress" + shortcut: "Shift+S" + checkable: true + onTriggered: console.log("Stress") + } + + Action { + id: silhouetteColorAction + text: "Silhouette" + shortcut: "Shift+T" + checkable: true + onTriggered: console.log("Silhouette") + } } } diff --git a/measures.cpp b/measures.cpp index 1a6e4ec..a5142a1 100644 --- a/measures.cpp +++ b/measures.cpp @@ -2,6 +2,80 @@ #include <cassert> #include <cmath> +#include <algorithm> + +static +void knn(const arma::mat &dmat, arma::uword i, arma::uword k, arma::uvec &nn, arma::vec &dist) +{ + arma::uword n = dist.n_rows; + if (k > n) { + return; + } + + for (arma::uword j = 0, l = 0; l < k; j++, l++) { + if (j == i) { + j++; + } + + nn[l] = j; + dist[l] = dmat(i, j); + } + + double dmax = *std::max_element(dist.begin(), dist.end()); + for (arma::uword j = 0; j < n; j++) { + if (j == i) { + continue; + } + + if (dmat(i, j) < dmax) { + dmax = dmat(i, j); + arma::uword l; + for (l = 0; dmat(i, j) > dist[l] && l < k; l++); + for (arma::uword m = l + 1; m < k; m++) { + nn[m] = nn[m - 1]; + dist[m] = dist[m - 1]; + } + + nn[l] = j; + dist[l] = dmat(i, j); + } + } +} + +arma::vec mp::neighborhoodPreservation(const arma::mat &distA, + const arma::mat &distB, + arma::uword k) +{ + arma::uword n = distA.n_rows; + arma::vec np(n); + + #pragma omp parallel for shared(np, n) + for (arma::uword i = 0; i < n; i++) { + arma::uvec nnA(k); + arma::uvec nnB(k); + arma::vec dist(k); + + knn(distA, i, k, nnA, dist); + knn(distB, i, k, nnB, dist); + + std::sort(nnA.begin(), nnA.end()); + std::sort(nnB.begin(), nnB.end()); + + arma::uword l; + for (l = 0; nnA[l] == nnB[l] && l < k; l++); + np[i] = ((double) l) / k; + } + + return np; +} + +arma::vec mp::silhouette(const arma::mat &distA, + const arma::mat &distB, + const arma::vec &labels) +{ + // TODO + return arma::vec(distA.n_rows, arma::fill::zeros); +} double mp::stress(const arma::mat &Dp, const arma::mat &Dq) { @@ -50,7 +124,7 @@ double mp::klDivergence(const arma::rowvec &pi, const arma::rowvec &qi) arma::mat mp::d2p(const arma::mat &D, const arma::vec &sigmas) { arma::mat P(D.n_rows, D.n_cols); - mp::relevance(D, sigmas, P); + mp::d2p(D, sigmas, P); return P; } @@ -1,10 +1,21 @@ +#ifndef MP_H +#define MP_H + #include <armadillo> namespace mp { +// Distance-related +typedef double (*DistFunc)(const arma::rowvec &, const arma::rowvec &); double euclidean(const arma::rowvec &x1, const arma::rowvec &x2); -arma::mat dist(const arma::mat &X, double (*distCalc)(const arma::rowvec &, const arma::rowvec &) = euclidean); +arma::mat dist(const arma::mat &X, DistFunc dfunc = euclidean); + +// Evaluation measures +typedef arma::vec (*MeasureFunc)(const arma::mat &distA, const arma::mat &distB); +arma::vec neighborhoodPreservation(const arma::mat &distA, const arma::mat &distB, arma::uword k = 10); +arma::vec silhouette(const arma::mat &distA, const arma::mat &distB, const arma::vec &labels); +// Techniques arma::mat lamp(const arma::mat &X, const arma::uvec &sampleIndices, const arma::mat &Ys); void lamp(const arma::mat &X, const arma::uvec &sampleIndices, const arma::mat &Ys, arma::mat &Y); @@ -14,3 +25,5 @@ arma::mat tSNE(const arma::mat &X, arma::uword k = 2, double perplexity = 30, ar void tSNE(const arma::mat &X, arma::mat &Y, double perplexity = 30, arma::uword nIter = 1000); } // namespace mp + +#endif // MP_H diff --git a/npdistortion.cpp b/npdistortion.cpp new file mode 100644 index 0000000..1994c74 --- /dev/null +++ b/npdistortion.cpp @@ -0,0 +1,13 @@ +#include "npdistortion.h" + +#include "mp.h" + +NPDistortion::NPDistortion(int k) + : m_k(k) +{ +} + +arma::vec NPDistortion::measure(const arma::mat &distA, const arma::mat &distB) +{ + return mp::neighborhoodPreservation(distA, distB, m_k); +} diff --git a/npdistortion.h b/npdistortion.h new file mode 100644 index 0000000..a70616d --- /dev/null +++ b/npdistortion.h @@ -0,0 +1,16 @@ +#ifndef NPDISTORTION_H +#define NPDISTORTION_H + +#include "distortionmeasure.h" + +class NPDistortion : public DistortionMeasure +{ +public: + NPDistortion(int k = 10); + arma::vec measure(const arma::mat &distA, const arma::mat &distB); + +private: + int m_k; +}; + +#endif // NPDISTORTION_H @@ -3,16 +3,33 @@ QT += qml quick widgets QMAKE_CXXFLAGS += -std=c++11 -fopenmp QMAKE_LIBS += -larmadillo -fopenmp HEADERS += colorscale.h \ + continuouscolorscale.h \ + geometry.h \ + scale.h \ scatterplot.h \ + historygraph.h \ interactionhandler.h \ + selectionhandler.h \ + effectivenessobserver.h \ + distortionobserver.h \ + distortionmeasure.h \ + npdistortion.h \ mp.h SOURCES += main.cpp \ colorscale.cpp \ + continuouscolorscale.cpp \ + geometry.cpp \ scatterplot.cpp \ + historygraph.cpp \ interactionhandler.cpp \ + selectionhandler.cpp \ + effectivenessobserver.cpp \ + distortionobserver.cpp \ + npdistortion.cpp \ lamp.cpp \ forceScheme.cpp \ tsne.cpp \ + measures.cpp \ dist.cpp RESOURCES += pm.qrc @@ -0,0 +1,81 @@ +#ifndef SCALE_H +#define SCALE_H + +#include <algorithm> + +class Scale +{ +public: + Scale(float domainMin, float domainMax, float rangeMin, float rangeMax) + : m_domainMin(domainMin) + , m_domainMax(domainMax) + , m_rangeMin(rangeMin) + , m_rangeMax(rangeMax) + { + valuesUpdated(); + } + + virtual ~Scale() {} + + void setRangeMin(float rangeMin) { m_rangeMin = rangeMin; valuesUpdated(); } + void setRangeMax(float rangeMax) { m_rangeMax = rangeMax; valuesUpdated(); } + void setDomainMin(float domainMin) { m_domainMin = domainMin; valuesUpdated(); } + void setDomainMax(float domainMax) { m_domainMax = domainMax; valuesUpdated(); } + + void setRange(float rangeMin, float rangeMax) + { + m_rangeMin = rangeMin; + m_rangeMax = rangeMax; + valuesUpdated(); + } + + void setDomain(float domainMin, float domainMax) + { + m_domainMin = domainMin; + m_domainMax = domainMax; + valuesUpdated(); + } + + void reverse() + { + std::swap(m_rangeMin, m_domainMin); + std::swap(m_rangeMax, m_domainMax); + valuesUpdated(); + } + + virtual float operator()(float value) const = 0; + +protected: + // Called when internal values change + virtual void valuesUpdated() {} + + float m_domainMin, m_domainMax; + float m_rangeMin, m_rangeMax; +}; + +class LinearScale + : public Scale +{ +public: + LinearScale(float domainMin, float domainMax, float rangeMin, float rangeMax) + : Scale(domainMin, domainMax, rangeMin, rangeMax) + { + valuesUpdated(); + } + + virtual float operator()(float value) const + { + return (value - m_domainMin) * m_transformSlope + m_rangeMin; + } + +protected: + virtual void valuesUpdated() + { + m_transformSlope = (m_rangeMax - m_rangeMin) / (m_domainMax - m_domainMin); + } + +private: + float m_transformSlope; +}; + +#endif // SCALE_H diff --git a/scatterplot.cpp b/scatterplot.cpp index 0c2fbc1..e22eafd 100644 --- a/scatterplot.cpp +++ b/scatterplot.cpp @@ -1,108 +1,135 @@ #include "scatterplot.h" +#include "geometry.h" #include <cmath> -static const qreal GLYPH_OPACITY = 0.3; +static const qreal GLYPH_OPACITY = 0.4; static const qreal GLYPH_OPACITY_SELECTED = 1.0; -static const QColor SELECTION_COLOR(QColor(128, 128, 128, 96)); -static const int GLYPH_SIZE = 8; -static const float PADDING = 10; -static const float PI = 3.1415f; + +static const QColor OUTLINE_COLOR(0, 0, 0); +static const QColor SELECTION_COLOR(128, 128, 128, 96); + +static const int GLYPH_SIZE = 8.f; +static const float PADDING = 10.f; Scatterplot::Scatterplot(QQuickItem *parent) : QQuickItem(parent) - , m_colorScale{ - QColor("#1f77b4"), - QColor("#ff7f0e"), - QColor("#2ca02c"), - QColor("#d62728"), - QColor("#9467bd"), - QColor("#8c564b"), - QColor("#e377c2"), - QColor("#17becf"), - QColor("#7f7f7f"), - } + , m_sx(0, 1, 0, 1) + , m_sy(0, 1, 0, 1) + , m_currentInteractionState(INTERACTION_NONE) + , m_shouldUpdateGeometry(false) + , m_shouldUpdateMaterials(false) { setClip(true); setFlag(QQuickItem::ItemHasContents); } -Scatterplot::~Scatterplot() -{ -} - -void Scatterplot::setData(const arma::mat &data) +void Scatterplot::setColorScale(ColorScale *colorScale) { - if (data.n_cols != 3) + if (!colorScale) { return; + } - m_data = data; - m_xmin = data.col(0).min(); - m_xmax = data.col(0).max(); - m_ymin = data.col(1).min(); - m_ymax = data.col(1).max(); - - m_colorScale.setExtents(m_data.col(2).min(), m_data.col(2).max()); - m_selectedGlyphs.clear(); + m_colorScale = colorScale; + if (m_colorData.n_elem > 0) { + updateMaterials(); + } +} - update(); +arma::mat Scatterplot::XY() const +{ + return m_xy; } -int calculateCircleVertexCount(qreal radius) +bool Scatterplot::saveToFile(const QUrl &url) { - // 10 * sqrt(r) \approx 2*pi / acos(1 - 1 / (4*r)) - return (int) (10.0 * sqrt(radius)); + if (!url.isLocalFile()) { + return false; + } + + return m_xy.save(url.path().toStdString(), arma::raw_ascii); } -void updateCircleGeometry(QSGGeometry *geometry, float size, float cx, float cy) +void Scatterplot::setXY(const arma::mat &xy) { - int vertexCount = geometry->vertexCount(); + if (xy.n_cols != 2) { + return; + } - float theta = 2 * PI / float(vertexCount); - float c = cosf(theta); - float s = sinf(theta); - float x = size / 2; - float y = 0; + if (m_xy.n_elem != xy.n_elem) { + m_selectedGlyphs.clear(); + } - QSGGeometry::Point2D *vertexData = geometry->vertexDataAsPoint2D(); - for (int i = 0; i < vertexCount; i++) { - vertexData[i].set(x + cx, y + cy); + m_xy = xy; + m_sx.setDomain(m_xy.col(0).min(), m_xy.col(0).max()); + m_sy.setDomain(m_xy.col(1).min(), m_xy.col(1).max()); - float t = x; - x = c*x - s*y; - y = s*t + c*y; + updateGeometry(); + + emit xyChanged(m_xy); +} + +void Scatterplot::setColorData(const arma::vec &colorData) +{ + if (colorData.n_elem != m_xy.n_rows) { + return; } + + m_colorData = colorData; + emit colorDataChanged(m_colorData); + + updateMaterials(); } -inline float Scatterplot::fromDataXToScreenX(float x) +void Scatterplot::updateGeometry() { - return PADDING + (x - m_xmin) / (m_xmax - m_xmin) * (width() - 2*PADDING); + m_shouldUpdateGeometry = true; + update(); } -inline float Scatterplot::fromDataYToScreenY(float y) +void Scatterplot::updateMaterials() { - return PADDING + (y - m_ymin) / (m_ymax - m_ymin) * (height() - 2*PADDING); + m_shouldUpdateMaterials = true; + update(); } -QSGNode *Scatterplot::newGlyphNodeTree() { +QSGNode *Scatterplot::createGlyphNodeTree() +{ + if (m_xy.n_rows < 1) { + return 0; + } + QSGNode *node = new QSGNode; int vertexCount = calculateCircleVertexCount(GLYPH_SIZE / 2); - for (arma::uword i = 0; i < m_data.n_rows; i++) { - QSGGeometryNode *glyphNode = new QSGGeometryNode; + for (arma::uword i = 0; i < m_xy.n_rows; i++) { + QSGGeometry *glyphOutlineGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); + glyphOutlineGeometry->setDrawingMode(GL_LINE_LOOP); + updateCircleGeometry(glyphOutlineGeometry, GLYPH_SIZE / 2, 0, 0); + QSGGeometryNode *glyphOutlineNode = new QSGGeometryNode; + glyphOutlineNode->setGeometry(glyphOutlineGeometry); + glyphOutlineNode->setFlag(QSGNode::OwnsGeometry); - QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); - geometry->setDrawingMode(GL_POLYGON); - glyphNode->setGeometry(geometry); + QSGFlatColorMaterial *material = new QSGFlatColorMaterial; + material->setColor(OUTLINE_COLOR); + glyphOutlineNode->setMaterial(material); + glyphOutlineNode->setFlag(QSGNode::OwnsMaterial); + + QSGGeometry *glyphGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); + glyphGeometry->setDrawingMode(GL_POLYGON); + updateCircleGeometry(glyphGeometry, GLYPH_SIZE / 2 - 0.5, 0, 0); + QSGGeometryNode *glyphNode = new QSGGeometryNode; + glyphNode->setGeometry(glyphGeometry); glyphNode->setFlag(QSGNode::OwnsGeometry); - QSGFlatColorMaterial *material = new QSGFlatColorMaterial; - material->setColor(m_colorScale.color(m_data(i, 2))); + material = new QSGFlatColorMaterial; + material->setColor(QColor()); glyphNode->setMaterial(material); glyphNode->setFlag(QSGNode::OwnsMaterial); // Place the glyph geometry node under an opacity node QSGOpacityNode *glyphOpacityNode = new QSGOpacityNode; + glyphOpacityNode->appendChildNode(glyphOutlineNode); glyphOpacityNode->appendChildNode(glyphNode); node->appendChildNode(glyphOpacityNode); } @@ -112,47 +139,75 @@ QSGNode *Scatterplot::newGlyphNodeTree() { QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { - if (m_data.n_rows < 1) - return 0; - - qreal x, y, xt, yt, moveTranslationF; - QSGNode *root = 0; if (!oldNode) { root = new QSGNode; - root->appendChildNode(newGlyphNodeTree()); + QSGNode *glyphTreeRoot = createGlyphNodeTree(); + if (glyphTreeRoot) { + root->appendChildNode(glyphTreeRoot); + } } else { root = oldNode; } - if (m_currentState != INTERACTION_MOVING) - xt = yt = 0; - else { - xt = m_dragCurrentPos.x() - m_dragOriginPos.x(); - yt = m_dragCurrentPos.y() - m_dragOriginPos.y(); + if (m_xy.n_rows < 1) { + return root; + } + + qreal x, y, tx, ty, moveTranslationF; + + if (m_currentInteractionState == INTERACTION_MOVING) { + tx = m_dragCurrentPos.x() - m_dragOriginPos.x(); + ty = m_dragCurrentPos.y() - m_dragOriginPos.y(); + } else { + tx = ty = 0; } + m_sx.setRange(PADDING, width() - 2*PADDING); + m_sy.setRange(height() - 2*PADDING, PADDING); + QSGNode *node = root->firstChild()->firstChild(); - for (arma::uword i = 0; i < m_data.n_rows; i++) { - arma::rowvec row = m_data.row(i); + for (arma::uword i = 0; i < m_xy.n_rows; i++) { + arma::rowvec row = m_xy.row(i); bool isSelected = m_selectedGlyphs.contains(i); QSGOpacityNode *glyphOpacityNode = static_cast<QSGOpacityNode *>(node); glyphOpacityNode->setOpacity(isSelected ? GLYPH_OPACITY_SELECTED : GLYPH_OPACITY); - QSGGeometryNode *glyphNode = static_cast<QSGGeometryNode *>(node->firstChild()); - QSGGeometry *geometry = glyphNode->geometry(); - moveTranslationF = isSelected ? 1.0 : 0.0; - x = fromDataXToScreenX(row[0]) + xt * moveTranslationF; - y = fromDataYToScreenY(row[1]) + yt * moveTranslationF; - updateCircleGeometry(geometry, GLYPH_SIZE, x, y); - glyphNode->markDirty(QSGNode::DirtyGeometry); + QSGGeometryNode *glyphOutlineNode = static_cast<QSGGeometryNode *>(node->firstChild()); + QSGGeometryNode *glyphNode = static_cast<QSGGeometryNode *>(node->firstChild()->nextSibling()); + if (m_shouldUpdateGeometry) { + moveTranslationF = isSelected ? 1.0 : 0.0; + x = m_sx(row[0]) + tx * moveTranslationF; + y = m_sy(row[1]) + ty * moveTranslationF; + + QSGGeometry *geometry = glyphOutlineNode->geometry(); + updateCircleGeometry(geometry, GLYPH_SIZE / 2, x, y); + glyphOutlineNode->markDirty(QSGNode::DirtyGeometry); + + geometry = glyphNode->geometry(); + updateCircleGeometry(geometry, GLYPH_SIZE / 2 - 0.5, x, y); + glyphNode->markDirty(QSGNode::DirtyGeometry); + } + if (m_shouldUpdateMaterials) { + QSGFlatColorMaterial *material = static_cast<QSGFlatColorMaterial *>(glyphNode->material()); + material->setColor(m_colorScale->color(m_colorData[i])); + glyphNode->setMaterial(material); + glyphNode->markDirty(QSGNode::DirtyMaterial); + } node = node->nextSibling(); } - // Draw selection - if (m_currentState == INTERACTION_SELECTING) { + if (m_shouldUpdateGeometry) { + m_shouldUpdateGeometry = false; + } + if (m_shouldUpdateMaterials) { + m_shouldUpdateMaterials = false; + } + + // Selection rect + if (m_currentInteractionState == INTERACTION_SELECTING) { QSGSimpleRectNode *selectionNode = 0; if (!root->firstChild()->nextSibling()) { selectionNode = new QSGSimpleRectNode; @@ -165,9 +220,11 @@ QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) selectionNode->setRect(QRectF(m_dragOriginPos, m_dragCurrentPos)); selectionNode->markDirty(QSGNode::DirtyGeometry); } else { - if (root->firstChild()->nextSibling()) { - root->firstChild()->nextSibling()->markDirty(QSGNode::DirtyGeometry); - root->removeChildNode(root->firstChild()->nextSibling()); + node = root->firstChild()->nextSibling(); + if (node) { + // node->markDirty(QSGNode::DirtyGeometry); + root->removeChildNode(node); + delete node; } } @@ -176,15 +233,19 @@ QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) void Scatterplot::mousePressEvent(QMouseEvent *event) { - switch (m_currentState) { + switch (m_currentInteractionState) { case INTERACTION_NONE: case INTERACTION_SELECTED: - m_currentState = (event->button() == Qt::MiddleButton) ? INTERACTION_MOVING - : INTERACTION_SELECTING; + if (event->modifiers() == Qt::AltModifier) { + m_currentInteractionState = INTERACTION_BEGIN_MOVING; + } else { + m_currentInteractionState = INTERACTION_SELECTING; + } m_dragOriginPos = event->localPos(); m_dragCurrentPos = m_dragOriginPos; break; case INTERACTION_SELECTING: + case INTERACTION_BEGIN_MOVING: case INTERACTION_MOVING: event->ignore(); return; @@ -193,18 +254,20 @@ void Scatterplot::mousePressEvent(QMouseEvent *event) void Scatterplot::mouseMoveEvent(QMouseEvent *event) { - switch (m_currentState) { + switch (m_currentInteractionState) { + case INTERACTION_NONE: + // event->localPos() + break; case INTERACTION_SELECTING: m_dragCurrentPos = event->localPos(); update(); break; + case INTERACTION_BEGIN_MOVING: + m_currentInteractionState = INTERACTION_MOVING; case INTERACTION_MOVING: m_dragCurrentPos = event->localPos(); - updateData(); - update(); - m_dragOriginPos = m_dragCurrentPos; + updateGeometry(); break; - case INTERACTION_NONE: case INTERACTION_SELECTED: event->ignore(); return; @@ -213,19 +276,23 @@ void Scatterplot::mouseMoveEvent(QMouseEvent *event) void Scatterplot::mouseReleaseEvent(QMouseEvent *event) { - bool mergeSelection; - - switch (m_currentState) { + switch (m_currentInteractionState) { case INTERACTION_SELECTING: - mergeSelection = (event->button() == Qt::RightButton); - m_currentState = selectGlyphs(mergeSelection) ? INTERACTION_SELECTED - : INTERACTION_NONE; - update(); + { + bool mergeSelection = (event->modifiers() == Qt::ControlModifier); + m_currentInteractionState = + updateSelection(mergeSelection) ? INTERACTION_SELECTED + : INTERACTION_NONE; + } + break; + case INTERACTION_BEGIN_MOVING: + m_currentInteractionState = INTERACTION_SELECTED; break; - case INTERACTION_MOVING: - m_currentState = INTERACTION_SELECTED; - update(); + m_currentInteractionState = INTERACTION_SELECTED; + applyManipulation(); + updateGeometry(); + m_dragOriginPos = m_dragCurrentPos; break; case INTERACTION_NONE: case INTERACTION_SELECTED: @@ -233,44 +300,64 @@ void Scatterplot::mouseReleaseEvent(QMouseEvent *event) } } -bool Scatterplot::selectGlyphs(bool mergeSelection) +bool Scatterplot::updateSelection(bool mergeSelection) { - if (!mergeSelection) - m_selectedGlyphs.clear(); + QSet<int> selection; + if (mergeSelection) { + selection.unite(m_selectedGlyphs); + } - qreal x, y; + m_sx.reverse(); + m_sy.reverse(); - QRectF selectionRect(m_dragOriginPos, m_dragCurrentPos); - bool anySelected = false; - for (arma::uword i = 0; i < m_data.n_rows; i++) { - arma::rowvec row = m_data.row(i); - x = fromDataXToScreenX(row[0]); - y = fromDataYToScreenY(row[1]); + float originX = m_sx(m_dragOriginPos.x()); + float originY = m_sy(m_dragOriginPos.y()); + float currentX = m_sx(m_dragCurrentPos.x()); + float currentY = m_sy(m_dragCurrentPos.y()); - if (selectionRect.contains(x, y)) { - m_selectedGlyphs.insert(i); - if (!anySelected) - anySelected = true; + m_sy.reverse(); + m_sx.reverse(); + + QRectF selectionRect(QPointF(originX, originY), QPointF(currentX, currentY)); + + for (arma::uword i = 0; i < m_xy.n_rows; i++) { + const arma::rowvec &row = m_xy.row(i); + + if (selectionRect.contains(row[0], row[1])) { + selection.insert(i); } } - return anySelected; + setSelection(selection); + return !selection.isEmpty(); } -void Scatterplot::updateData() +void Scatterplot::setSelection(const QSet<int> &selection) { - float xt = m_dragCurrentPos.x() - m_dragOriginPos.x(); - float yt = m_dragCurrentPos.y() - m_dragOriginPos.y(); + m_selectedGlyphs = selection; + update(); + + emit selectionChanged(selection); +} + +void Scatterplot::applyManipulation() +{ + m_sx.reverse(); + m_sy.reverse(); + LinearScale rx = m_sx; + LinearScale ry = m_sy; + m_sy.reverse(); + m_sx.reverse(); + + float tx = m_dragCurrentPos.x() - m_dragOriginPos.x(); + float ty = m_dragCurrentPos.y() - m_dragOriginPos.y(); - xt /= (width() - PADDING); - yt /= (height() - PADDING); for (auto it = m_selectedGlyphs.cbegin(); it != m_selectedGlyphs.cend(); it++) { - arma::rowvec row = m_data.row(*it); - row[0] = ((row[0] - m_xmin) / (m_xmax - m_xmin) + xt) * (m_xmax - m_xmin) + m_xmin; - row[1] = ((row[1] - m_ymin) / (m_ymax - m_ymin) + yt) * (m_ymax - m_ymin) + m_ymin; - m_data.row(*it) = row; + arma::rowvec row = m_xy.row(*it); + row[0] = rx(m_sx(row[0]) + tx); + row[1] = ry(m_sy(row[1]) + ty); + m_xy.row(*it) = row; } - // does not send last column (labels) - emit dataChanged(m_data.cols(0, m_data.n_cols - 2)); + emit xyInteractivelyChanged(m_xy); } diff --git a/scatterplot.h b/scatterplot.h index a155daf..2dc031a 100644 --- a/scatterplot.h +++ b/scatterplot.h @@ -1,23 +1,33 @@ #ifndef SCATTERPLOT_H #define SCATTERPLOT_H +#include <memory> #include <armadillo> #include <QtQuick> #include "colorscale.h" +#include "scale.h" class Scatterplot : public QQuickItem { Q_OBJECT public: Scatterplot(QQuickItem *parent = 0); - ~Scatterplot(); + + arma::mat XY() const; + void setColorScale(ColorScale *colorScale); + Q_INVOKABLE bool saveToFile(const QUrl &url); signals: - void dataChanged(const arma::mat &data); + void xyChanged(const arma::mat &XY) const; + void xyInteractivelyChanged(const arma::mat &XY) const; + void colorDataChanged(const arma::vec &colorData) const; + void selectionChanged(const QSet<int> &selection) const; public slots: - void setData(const arma::mat &data); + void setXY(const arma::mat &xy); + void setColorData(const arma::vec &colorData); + void setSelection(const QSet<int> &selection); protected: QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *); @@ -26,27 +36,33 @@ protected: void mouseReleaseEvent(QMouseEvent *event); private: - QSGNode *newGlyphNodeTree(); - bool selectGlyphs(bool mergeSelection); - void updateData(); + QSGNode *createGlyphNodeTree(); + bool updateSelection(bool mergeSelection); - float fromDataXToScreenX(float x); - float fromDataYToScreenY(float y); + void applyManipulation(); - arma::mat m_data; - float m_xmin, m_xmax, m_ymin, m_ymax; + void updateGeometry(); + void updateMaterials(); - ColorScale m_colorScale; + arma::mat m_xy; + LinearScale m_sx, m_sy; enum InteractionState { INTERACTION_NONE, INTERACTION_SELECTING, INTERACTION_SELECTED, + INTERACTION_BEGIN_MOVING, INTERACTION_MOVING - } m_currentState; + } m_currentInteractionState; + QPointF m_dragOriginPos, m_dragCurrentPos; QSet<int> m_selectedGlyphs; + + bool m_shouldUpdateGeometry, m_shouldUpdateMaterials; + + arma::vec m_colorData; + ColorScale *m_colorScale; }; #endif // SCATTERPLOT_H diff --git a/selectionhandler.cpp b/selectionhandler.cpp new file mode 100644 index 0000000..44eef28 --- /dev/null +++ b/selectionhandler.cpp @@ -0,0 +1,19 @@ +#include "selectionhandler.h" + +SelectionHandler::SelectionHandler(const arma::uvec &sampleIndices) + : m_sampleIndices(sampleIndices) +{ +} + +void SelectionHandler::setSelection(const QSet<int> &selection) +{ + QSet<int> newSelection; + + // The selecion happens over the sample indices. We use the original dataset + // indices in sampleIndices to translate indices. + for (auto it = selection.begin(); it != selection.end(); it++) { + newSelection.insert(m_sampleIndices[*it]); + } + + emit selectionChanged(newSelection); +} diff --git a/selectionhandler.h b/selectionhandler.h new file mode 100644 index 0000000..e5b9686 --- /dev/null +++ b/selectionhandler.h @@ -0,0 +1,24 @@ +#ifndef SELECTIONHANDLER_H +#define SELECTIONHANDLER_H + +#include <QObject> +#include <QSet> +#include <armadillo> + +class SelectionHandler : public QObject +{ + Q_OBJECT +public: + SelectionHandler(const arma::uvec &sampleIndices); + +signals: + void selectionChanged(const QSet<int> &selection); + +public slots: + void setSelection(const QSet<int> &selection); + +private: + arma::uvec m_sampleIndices; +}; + +#endif // SELECTIONHANDLER_H @@ -68,8 +68,9 @@ void mp::tSNE(const arma::mat &X, arma::mat &Y, double perplexity, arma::uword n Y += iY; Y.each_row() -= mean(Y, 0); - if (iter == EXAGGERATION_THRESHOLD_ITER) + if (iter == EXAGGERATION_THRESHOLD_ITER) { P /= EARLY_EXAGGERATION; // remove early exaggeration + } } } @@ -94,19 +95,21 @@ static void calcP(const arma::mat &X, arma::mat &P, double perplexity, double to for (int tries = 0; fabs(hDiff) > tol && tries < MAX_BINSEARCH_TRIES; tries++) { if (hDiff > 0) { betaMin = beta[i]; - if (betaMax == arma::datum::inf || betaMax == -arma::datum::inf) + if (betaMax == arma::datum::inf || betaMax == -arma::datum::inf) { beta[i] *= 2; - else + } else { beta[i] = (beta[i] + betaMax) / 2.; + } } else { betaMax = beta[i]; - if (betaMin == arma::datum::inf || betaMin == -arma::datum::inf) + if (betaMin == arma::datum::inf || betaMin == -arma::datum::inf) { beta[i] /= 2; - else + } else { beta[i] = (beta[i] + betaMin) / 2.; + } } - + h = hBeta(Di, beta[i], Pi); hDiff = h - logU; } |