From 2f00565fb0ae84fdb2f7333cb9a6dcb3291151cd Mon Sep 17 00:00:00 2001
From: Eike Cochu <eike@cochu.com>
Date: Fri, 5 Feb 2016 21:16:43 +0100
Subject: [PATCH] improved network graph

now adding links between already loaded nodes and new nodes
fixed bug where loading would create double links
improved style
added legend
---
 .../app/html/articles/detail/network.html     |  11 +-
 vipra-ui/app/js/controllers.js                | 117 -------------
 vipra-ui/app/js/directives.js                 | 161 ++++++++++++++++--
 vipra-ui/app/less/app.less                    |  14 +-
 .../main/java/de/vipra/util/Constants.java    |   4 +-
 5 files changed, 175 insertions(+), 132 deletions(-)

diff --git a/vipra-ui/app/html/articles/detail/network.html b/vipra-ui/app/html/articles/detail/network.html
index 5ba521ab..d1e0899b 100644
--- a/vipra-ui/app/html/articles/detail/network.html
+++ b/vipra-ui/app/html/articles/detail/network.html
@@ -1 +1,10 @@
-<div class="graph" vis-graph="graph" vis-data="data" vis-options="options" vis-select="select" vis-dblclick="open"></div>
\ No newline at end of file
+<div class="graph"
+    vis-graph
+    vis-type="article"
+    ng-model="article">
+
+    <div class="graph-legend">
+      <span style="color:#BBC9D2">Articles</span><br>
+      <span style="color:#DBB234">Topics</span><br>
+    </div>
+</div>
\ No newline at end of file
diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js
index 8fb66204..79283037 100644
--- a/vipra-ui/app/js/controllers.js
+++ b/vipra-ui/app/js/controllers.js
@@ -88,123 +88,6 @@
 
     ArticleFactory.get({id: $stateParams.id}, function(response) {
       $scope.article = response.data;
-
-      $scope.nodes = new vis.DataSet();
-      $scope.edges = new vis.DataSet();
-      $scope.data = {
-        nodes: $scope.nodes,
-        edges: $scope.edges
-      };
-      $scope.options = {
-        nodes: {
-          font: {
-            size: 11
-          },
-          scaling: {
-            label: true
-          },
-          shadow: true
-        },
-        layout: {
-          randomSeed: 1
-        },
-        physics: {
-          barnesHut: {
-            centralGravity: 0.1,
-            springLength: 200,
-            springConstant: 0.01,
-            avoidOverlap: 0.1
-          }
-        }
-      };
-
-      // initial network
-      var id = 0,
-          ids = {},
-          nodes = [],
-          edges = [],
-          topics = $scope.article.topics,
-          articleColor = '#BBC9D2',
-          topicColor = '#DBB234';
-      
-      // root node
-      nodes.push({id: ++id, title: $scope.article.title, label: $scope.article.title.ellipsize(20), type: 'article', article: $scope.article.id, loaded: true, color: {background: articleColor}});
-      ids[$scope.article.id] = id;
-      
-      // child nodes
-      for(var i = 0; i < topics.length; i++) {
-        var topic = topics[i].topic;
-        nodes.push({id: ++id, label: topic.name, type: 'topic', topic: topic.id, color: {background: topicColor}});
-        edges.push({from: 1, to: id});
-        ids[topic.id] = id;
-      }
-
-      $scope.nodes.add(nodes);
-      $scope.edges.add(edges);
-      
-      // on node select
-      $scope.select = function(props) {
-        var node = $scope.nodes.get(props.nodes[0]);
-        if(node && !node.loaded) {
-          if(node.type === 'topic') {
-            // node is topic, load topic to get articles
-            TopicFactory.get({id:node.topic}, function(res) {
-              if(res.data && res.data.articles) {
-                var articles = res.data.articles;
-                var newNodes = [];
-                var newEdges = [];
-                for(var i = 0; i < articles.length; i++) {
-                  var article = articles[i];
-                  if(!ids.hasOwnProperty(article.id)) {
-                    newNodes.push({id: ++id, title: article.title, label: article.title.ellipsize(20), type: 'article', article: article.id, color: {background: articleColor}});
-                    newEdges.push({from:node.id, to:id});
-                    ids[article.id] = id;
-                  }
-                }
-                if(newNodes.length)
-                  $scope.nodes.add(newNodes);
-                if(newEdges.length)
-                  $scope.edges.add(newEdges);
-              }
-            });
-          } else if(node.type === 'article') {
-            // node is article, load article to get topics
-            ArticleFactory.get({id:node.article}, function(res) {
-              if(res.data && res.data.topics) {
-                var topics = res.data.topics;
-                var newNodes = [];
-                var newEdges = [];
-                for(var i = 0; i < topics.length; i++) {
-                  var topic = topics[i].topic;
-                  if(!ids.hasOwnProperty(topic.id)) {
-                    newNodes.push({id: ++id, title: topic.name, label: topic.name.ellipsize(20), type: 'topic', topic: topic.id, color: {background: topicColor}});
-                    newEdges.push({from:node.id, to:id});
-                    ids[topic.id] = id;
-                  }
-                }
-                if(newNodes.length)
-                  $scope.nodes.add(newNodes);
-                if(newEdges.length)
-                  $scope.edges.add(newEdges);
-              }
-            });
-          }
-          node.loaded = true;
-          $scope.nodes.update(node);
-        }
-      };
-
-      // on node open
-      $scope.open = function(props) {
-        var node = $scope.nodes.get(props.nodes[0]);
-        if(node) {
-          if(node.type === 'article') {
-            $state.transitionTo('articles.detail.show', {id:node.article});
-          } else if(node.type === 'topic') {
-            $state.transitionTo('topics.show', {id:node.topic});
-          }
-        }
-      };
     });
 
   }]);
diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js
index 519f1d34..f86e7fcd 100644
--- a/vipra-ui/app/js/directives.js
+++ b/vipra-ui/app/js/directives.js
@@ -56,23 +56,164 @@
     };
   });
 
-  app.directive('visGraph', function() {
+  app.directive('visGraph', ['$state', 'ArticleFactory', 'TopicFactory',
+    function($state, ArticleFactory, TopicFactory) {
+
     return {
       scope: {
-        visGraph: '=',
+        ngModel: '=',
+        visType: '@',
         visData: '=',
-        visOptions: '=',
-        visSelect: '&',
-        visDblclick: '&'
+        visOptions: '='
       },
+      transclude: true,
+      template: '<ng-transclude/><div class="graph" id="visgraph"></div>',
       link: function($scope, $element) {
-        $scope.$watchGroup(['visData', 'visOptions'], function() {
-          $scope.visGraph = new vis.Network($element[0], $scope.visData, $scope.visOptions);
-          $scope.visGraph.on('selectNode', $scope.visSelect() || function() {});
-          $scope.visGraph.on('doubleClick', $scope.visDblclick() || function() {});
+        var id = 0,
+            ids = {},
+            nodes = [],
+            edges = [],
+            articleColor = '#BBC9D2',
+            topicColor = '#DBB234',
+            container = $element.find("#visgraph")[0];
+
+        $scope.articleColor = articleColor;
+        $scope.topicColor = topicColor;
+        $scope.nodes = new vis.DataSet();
+        $scope.edges = new vis.DataSet();
+        $scope.data = {
+          nodes: $scope.nodes,
+          edges: $scope.edges
+        };
+
+        $scope.options = {
+          nodes: {
+            font: { size: 11 },
+            shape: 'dot',
+            borderWidth: 0
+          },
+          layout: { randomSeed: 1 },
+          physics: {
+            barnesHut: {
+              springConstant: 0.005,
+              gravitationalConstant: -5000
+            }
+          }
+        };
+
+        var topicNode = function(topic) {
+          return {
+            id: ++id,
+            title: topic.name,
+            label: topic.name.ellipsize(20),
+            type: 'topic',
+            topic: topic.id,
+            color: {
+              background: topicColor,
+              highlight: { background: topicColor }
+            }
+          };
+        };
+
+        var articleNode = function(article) {
+          return {
+            id: ++id,
+            title: article.title,
+            label: article.title.ellipsize(20),
+            type: 'article',
+            article: article.id,
+            color: {
+              background: articleColor,
+              highlight: { background: articleColor }
+            }
+          };
+        };
+        
+        // on node select
+        $scope.select = function(props) {
+          var node = $scope.nodes.get(props.nodes[0]);
+          if(node && !node.loaded) {
+            if(node.type === 'article') {
+              // node is article, load article to get topics
+              ArticleFactory.get({id:node.article}, function(res) {
+                if(res.data && res.data.topics) {
+                  var topics = res.data.topics,
+                      newNodes = [],
+                      newEdges = [];
+                  for(var i = 0; i < topics.length; i++) {
+                    var topic =  topics[i].topic;
+                    if(ids.hasOwnProperty(topic.id)) {
+                      if(!$scope.nodes.get(ids[topic.id]).loaded)
+                        newEdges.push({from:node.id, to:ids[topic.id]});
+                    } else {
+                      newNodes.push(topicNode(topic));
+                      newEdges.push({from:node.id, to:id});
+                      ids[topic.id] = id;
+                    }
+                  }
+                  if(newNodes.length) $scope.nodes.add(newNodes);
+                  if(newEdges.length) $scope.edges.add(newEdges);
+                }
+              });
+            } else {
+              // node is topic, load topic to get articles
+              TopicFactory.get({id:node.topic}, function(res) {
+                if(res.data && res.data.articles) {
+                  var articles = res.data.articles,
+                      newNodes = [],
+                      newEdges = [];
+                  for(var i = 0; i < articles.length; i++) {
+                    var article = articles[i];
+                    if(ids.hasOwnProperty(article.id)) {
+                      if(!$scope.nodes.get(ids[article.id]).loaded)
+                        newEdges.push({from:ids[article.id], to:node.id});
+                    } else {
+                      newNodes.push(articleNode(article));
+                      newEdges.push({from:id, to:node.id});
+                      ids[article.id] = id;
+                    }
+                  }
+                  if(newNodes.length) $scope.nodes.add(newNodes);
+                  if(newEdges.length) $scope.edges.add(newEdges);
+                }
+              });
+            }
+            node.loaded = true;
+            $scope.nodes.update(node);
+          }
+        };
+
+        // on node open
+        $scope.open = function(props) {
+          var node = $scope.nodes.get(props.nodes[0]);
+          if(node.type === 'article')
+            $state.transitionTo('articles.detail.show', {id:node.article});
+          else
+            $state.transitionTo('topics.show', {id:node.topic});
+        };
+
+        // watch for changes to model
+        $scope.$watch('ngModel', function(newVal, oldVal) {
+          if(!newVal) return;
+
+          // root node
+          if($scope.visType === 'article')
+            nodes.push(articleNode($scope.ngModel));
+          else
+            nodes.push(topicNode($scope.ngModel));
+          ids[$scope.ngModel.id] = id;
+
+          // add nodes and edges
+          $scope.nodes.add(nodes);
+          $scope.edges.add(edges);
+
+          // create graph
+          $scope.visGraph = new vis.Network(container, $scope.data, $scope.options);
+          $scope.visGraph.on('selectNode', $scope.select);
+          $scope.visGraph.on('doubleClick', $scope.open);
         });
       }
     };
-  });
+  }]);
 
 })();
\ No newline at end of file
diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less
index 0391c334..9d966708 100644
--- a/vipra-ui/app/less/app.less
+++ b/vipra-ui/app/less/app.less
@@ -82,7 +82,7 @@ body {
   left: 0;
   right: 0;
   bottom: 0;
-  background: rgba(0,0,0,0.2);
+  background: rgba(0,0,0,0.1);
   content: " ";
   z-index: 9999;
 }
@@ -101,12 +101,22 @@ body {
 
 .graph {
   position: absolute;
-  top: 50px;
+  top: 10px;
   left: 0;
   right: 0;
   bottom: 50px;
 }
 
+.graph-legend {
+  position: absolute;
+  top: 60px;
+  left: 10px;
+  font-weight: bold;
+  padding: 10px;
+  border: 1px solid #aaa;
+  border-radius: 3px;
+}
+
 .noselect {
   -webkit-touch-callout: none;
   -webkit-user-select: none;
diff --git a/vipra-util/src/main/java/de/vipra/util/Constants.java b/vipra-util/src/main/java/de/vipra/util/Constants.java
index 556a5e84..449dabc7 100644
--- a/vipra-util/src/main/java/de/vipra/util/Constants.java
+++ b/vipra-util/src/main/java/de/vipra/util/Constants.java
@@ -50,13 +50,13 @@ public class Constants {
 	 * Number of topics to discover with topic modeling, if the selected topic
 	 * modeling library supports this parameter.
 	 */
-	public static final int K_TOPICS = 20;
+	public static final int K_TOPICS = 50;
 
 	/**
 	 * Number of words in a discovered topic, if the selected topic modeling
 	 * library supports this parameter.
 	 */
-	public static final int K_TOPIC_WORDS = 50;
+	public static final int K_TOPIC_WORDS = 80;
 
 	/**
 	 * Precision of likeliness numbers. Likeliness is calculated for words to
-- 
GitLab