aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--colorscale.cpp12
-rw-r--r--colorscale.h6
-rw-r--r--continuouscolorscale.cpp556
-rw-r--r--continuouscolorscale.h21
-rw-r--r--dist.cpp7
-rw-r--r--distortionmeasure.h12
-rw-r--r--distortionobserver.cpp33
-rw-r--r--distortionobserver.h29
-rw-r--r--effectivenessobserver.cpp42
-rw-r--r--effectivenessobserver.h26
-rw-r--r--forceScheme.cpp10
-rw-r--r--geometry.cpp39
-rw-r--r--geometry.h13
-rw-r--r--historygraph.cpp325
-rw-r--r--historygraph.h38
-rw-r--r--interactionhandler.cpp5
-rw-r--r--interactionhandler.h6
-rw-r--r--lamp.cpp3
-rw-r--r--main.cpp89
-rw-r--r--main_view.qml159
-rw-r--r--measures.cpp76
-rw-r--r--mp.h15
-rw-r--r--npdistortion.cpp13
-rw-r--r--npdistortion.h16
-rw-r--r--pm.pro17
-rw-r--r--scale.h81
-rw-r--r--scatterplot.cpp343
-rw-r--r--scatterplot.h40
-rw-r--r--selectionhandler.cpp19
-rw-r--r--selectionhandler.h24
-rw-r--r--tsne.cpp15
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
diff --git a/dist.cpp b/dist.cpp
index aaa2167..422aa10 100644
--- a/dist.cpp
+++ b/dist.cpp
@@ -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;
};
diff --git a/lamp.cpp b/lamp.cpp
index b7a1027..7d36877 100644
--- a/lamp.cpp
+++ b/lamp.cpp
@@ -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;
}
}
diff --git a/main.cpp b/main.cpp
index 5da297f..559d554 100644
--- a/main.cpp
+++ b/main.cpp
@@ -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;
}
diff --git a/mp.h b/mp.h
index 7d6b535..192f106 100644
--- a/mp.h
+++ b/mp.h
@@ -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
diff --git a/pm.pro b/pm.pro
index 3ca2aaa..92b13e7 100644
--- a/pm.pro
+++ b/pm.pro
@@ -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
diff --git a/scale.h b/scale.h
new file mode 100644
index 0000000..bfe18ae
--- /dev/null
+++ b/scale.h
@@ -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
diff --git a/tsne.cpp b/tsne.cpp
index 50022eb..9ef1f9f 100644
--- a/tsne.cpp
+++ b/tsne.cpp
@@ -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;
}