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