From af9b05d424b692e191e2bf0db315b6b2824883fe Mon Sep 17 00:00:00 2001
From: Eike Cochu <eike@cochu.com>
Date: Mon, 28 Mar 2016 22:48:54 +0200
Subject: [PATCH] added word evolution to topic show

---
 .../main/java/de/vipra/cmd/lda/Analyzer.java  |  8 ++-
 vipra-ui/app/html/articles/show.html          |  3 ++
 vipra-ui/app/html/topics/show.html            | 49 +++++++++--------
 vipra-ui/app/js/controllers.js                | 53 ++++++++++++++++++-
 vipra-ui/app/less/app.less                    | 14 +++++
 .../java/de/vipra/util/model/TopicWord.java   | 10 ++++
 6 files changed, 110 insertions(+), 27 deletions(-)

diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java b/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java
index ff261324..536eb3f5 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java
@@ -301,9 +301,15 @@ public class Analyzer {
 				final TopicWord newTopicWord = new TopicWord();
 				newTopicWord.setWord(sequenceWord.getWord());
 				final List<Double> sequenceProbabilities = new ArrayList<>(windowCount);
-				for (final double probability : probabilities[wordIndex.index(sequenceWord.getWord())])
+				final List<Double> sequenceProbabilitiesChange = new ArrayList<>(windowCount);
+				double prevProbability = 0;
+				for (final double probability : probabilities[wordIndex.index(sequenceWord.getWord())]) {
 					sequenceProbabilities.add(probability);
+					sequenceProbabilitiesChange.add(probability - prevProbability);
+					prevProbability = probability;
+				}
 				newTopicWord.setSequenceProbabilities(sequenceProbabilities);
+				newTopicWord.setSequenceProbabilitiesChange(sequenceProbabilitiesChange);
 				newTopicWords.add(newTopicWord);
 			}
 			newTopic.setWords(newTopicWords);
diff --git a/vipra-ui/app/html/articles/show.html b/vipra-ui/app/html/articles/show.html
index d5c2dc0b..6c682de6 100644
--- a/vipra-ui/app/html/articles/show.html
+++ b/vipra-ui/app/html/articles/show.html
@@ -69,6 +69,9 @@
           <small class="text-muted percent-align" ng-bind-template="({{(topic.share*100).toFixed(0)}}%)"></small>
           <topic-link topic="topic.topic" />
         </li>
+        <li class="text-muted" ng-show="!article.topics">
+          None
+        </li>
       </ul>
       <span class="text-muted" ng-hide="article.topics.length > 0">No topics</span>
       <div class="pie-chart" id="topic-share" highcharts="topicShare"></div>
diff --git a/vipra-ui/app/html/topics/show.html b/vipra-ui/app/html/topics/show.html
index 73e3d2eb..c94f2b37 100644
--- a/vipra-ui/app/html/topics/show.html
+++ b/vipra-ui/app/html/topics/show.html
@@ -69,9 +69,12 @@
             <a class="btn btn-sm btn-default" ng-model="topicsShowModels.relChartstyle" bs-radio="'areaspline'">Area</a>
             <a class="btn btn-sm btn-default" ng-model="topicsShowModels.relChartstyle" bs-radio="'spline'">Line</a>
           </div>
+          <div class="pull-right">
+            <a tabindex="0" class="btn btn-sm btn-default" ng-click="resetRelZoom()">Reset zoom</a>
+          </div>
         </div>
         <div class="panel-body">
-          <div class="area-chart" id="topic-seq" highcharts="topicSeq"></div>
+          <div class="chart area-chart" id="topicRelChart" highcharts="topicSeq"></div>
         </div>
       </div>
     </div>
@@ -92,38 +95,34 @@
             <a class="btn btn-sm btn-default" ng-model="topicsShowModels.wordChartstyle" bs-radio="'areaspline'">Area</a>
             <a class="btn btn-sm btn-default" ng-model="topicsShowModels.wordChartstyle" bs-radio="'spline'">Line</a>
           </div>
+          <div class="pull-right">
+            <a tabindex="0" class="btn btn-sm btn-default" ng-click="resetWordZoom()" ng-show="wordsSelected">Reset zoom</a>
+          </div>
         </div>
         <div class="panel-body">
-          <div class="area-chart" id="topic-word" highcharts="topicWord"></div>
-        </div>
-      </div>
-    </div>
-  </div>
-  <div class="row">
-    <div class="col-md-12">
-      <h3><anchor-link fragment="words" />Topic Words </h3>
-      <div class="panel panel-default">
-        <table class="table table-condensed table-bordered table-hover">
-          <thead>
-            <tr>
-              <th ng-model="topicsShowModels.topicSortwords" sort-by="word">Word</th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr ng-repeat="word in topic.words | orderBy:topicsShowModels.topicSortwords">
-              <td ng-bind="word.word"></td>
-            </tr>
-          </tbody>
-        </table>
-        <div class="panel-footer">
-          <ng-pluralize count="topic.words.length||0" when="{0:'No words',1:'Top word',other:'Top {} words'}"></ng-pluralize>
+          <div class="row row-full">
+            <div class="col-md-2">
+              <ul class="list-unstyled">
+                <li ng-repeat="word in topic.words">
+                  <div class="checkbox checkbox-condensed" ng-class="{selected:word.selected}">
+                    <input tabindex="0" type="checkbox" ng-model="word.selected" ng-attr-id="{{::word.word}}" ng-change="redrawWordEvolutionChart()">
+                    <label class="check" ng-attr-for="{{::word.word}}" ng-bind="::word.word"></label>
+                  </div>
+                </li>
+              </ul>
+            </div>
+            <div class="col-md-10 message-container">
+              <div class="chart area-chart" id="topicWordChart" highcharts="topicWord"></div>
+              <div class="message text-muted" ng-hide="wordsSelected">No words selected</div>
+            </div>
+          </div>
         </div>
       </div>
     </div>
   </div>
   <div class="row">
     <div class="col-md-12">
-      <h3><anchor-link fragment="sequences" />Sequence Words</h3>
+      <h3><anchor-link fragment="sequences" />Sequences</h3>
       <div class="panel panel-default">
         <div class="panel-heading">
           <small>Sequence:</small>
diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js
index db5ca8c4..13e979f2 100644
--- a/vipra-ui/app/js/controllers.js
+++ b/vipra-ui/app/js/controllers.js
@@ -725,7 +725,7 @@
         relSeqstyle: 'absolute',
         relChartstyle: 'areaspline',
         wordSeqstyle: 'absolute',
-        wordChartstyle: 'areaspline',
+        wordChartstyle: 'spline',
         topicSortwords: '-probability',
         seqSortwords: '-probability'
       };
@@ -741,8 +741,15 @@
         if (!angular.isObject($scope.rootModels.topicModel))
           $scope.rootModels.topicModel = data.topicModel;
 
+        // preselect some words
+        if($scope.topic.words) {
+          for(var i = 0; i < Math.min(3, $scope.topic.words.length); i++)
+            $scope.topic.words[i].selected = true;
+        }
+
         $timeout(function() {
           $scope.redrawRelevanceGraph();
+          $scope.redrawWordEvolutionChart();
         }, 0);
       }, function(err) {
         $scope.errors = err;
@@ -766,6 +773,47 @@
         }], $scope.topicsShowModels.relChartstyle);
       };
 
+      $scope.redrawWordEvolutionChart = function() {
+        if(!$scope.topic || !$scope.topic.words || !$scope.topic.sequences) return;
+        var evolutions = [];
+
+        // create series
+        for(var i = 0, word, probs; i < $scope.topic.words.length; i++) {
+          word = $scope.topic.words[i];
+          if(!word.selected) continue;
+          probs = [];
+          for(var j = 0, prob; j < word.sequenceProbabilities.length; j++) {
+            prob = $scope.topicsShowModels.wordSeqstyle === 'relative' ? word.sequenceProbabilitiesChange[j] : word.sequenceProbabilities[j];
+            probs.push([new Date($scope.topic.sequences[j].window.startDate).getTime(), prob]);
+          }
+          evolutions.push({
+            name: word.word,
+            data: probs
+          });
+        }
+
+        $scope.topicWord = areaRelevanceChart(evolutions, $scope.topicsShowModels.wordChartstyle);
+        $scope.wordsSelected = evolutions.length;
+      };
+
+      var topicRelChartElement = $('#topicRelChart');
+      $scope.resetRelZoom = function() {
+        if (!$scope.topic) return;
+        var highcharts = topicRelChartElement.highcharts();
+        if (!highcharts) return;
+
+        highcharts.xAxis[0].setExtremes(null, null);
+      };
+
+      var topicWordChartElement = $('#topicWordChart');
+      $scope.resetWordZoom = function() {
+        if (!$scope.wordsSelected) return;
+        var highcharts = topicWordChartElement.highcharts();
+        if (!highcharts) return;
+
+        highcharts.xAxis[0].setExtremes(null, null);
+      };
+
       $scope.startRename = function() {
         $scope.origName = $scope.topic.name;
         $scope.isRename = true;
@@ -811,6 +859,9 @@
       $scope.$watch('topicsShowModels.relSeqstyle', $scope.redrawRelevanceGraph);
       $scope.$watch('topicsShowModels.relChartstyle', $scope.redrawRelevanceGraph);
 
+      $scope.$watch('topicsShowModels.wordSeqstyle', $scope.redrawWordEvolutionChart);
+      $scope.$watch('topicsShowModels.wordChartstyle', $scope.redrawWordEvolutionChart);
+
       $scope.$watchGroup(['sequenceId'], function() {
         if (!$scope.sequenceId) return;
 
diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less
index 9cefe0e5..1cad561e 100644
--- a/vipra-ui/app/less/app.less
+++ b/vipra-ui/app/less/app.less
@@ -335,6 +335,7 @@ a:hover {
   color: #ccc;
 }
 
+.chart,
 .highcharts-container {
   width: 100% !important;
 }
@@ -418,6 +419,19 @@ topic-menu {
   }
 }
 
+.row.row-full {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display:         flex;
+  flex-wrap: wrap;
+
+  & > [class*='col-'] {
+    display: flex;
+    flex-direction: column;
+  }
+}
+
 @-moz-keyframes spin {
   100% {
     -moz-transform: rotateY(360deg);
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicWord.java b/vipra-util/src/main/java/de/vipra/util/model/TopicWord.java
index 05faecfc..74bbe6cf 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TopicWord.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TopicWord.java
@@ -13,6 +13,8 @@ public class TopicWord implements Comparable<TopicWord>, Serializable {
 
 	private List<Double> sequenceProbabilities;
 
+	private List<Double> sequenceProbabilitiesChange;
+
 	public String getWord() {
 		return word;
 	}
@@ -29,6 +31,14 @@ public class TopicWord implements Comparable<TopicWord>, Serializable {
 		this.sequenceProbabilities = sequenceProbabilities;
 	}
 
+	public List<Double> getSequenceProbabilitiesChange() {
+		return sequenceProbabilitiesChange;
+	}
+
+	public void setSequenceProbabilitiesChange(List<Double> sequenceProbabilitiesChange) {
+		this.sequenceProbabilitiesChange = sequenceProbabilitiesChange;
+	}
+
 	@Override
 	public int compareTo(final TopicWord o) {
 		return word.compareTo(o.getWord());
-- 
GitLab