From e26ce548c0383a844194ec23493216e968f71c1a Mon Sep 17 00:00:00 2001
From: Eike Cochu <eike@cochu.com>
Date: Thu, 28 Apr 2016 19:56:38 +0200
Subject: [PATCH] updated explorer, added window to article

---
 .../de/vipra/rest/resource/TopicResource.java |   4 +-
 .../vipra/rest/resource/WindowResource.java   | 100 ++++++++++++++
 vipra-cmd/runcfg/CMD.launch                   |   2 +-
 .../vipra/cmd/file/FilebaseWindowIndex.java   |  16 +++
 .../main/java/de/vipra/cmd/lda/Analyzer.java  |   3 +-
 .../vipra/cmd/option/DeleteModelCommand.java  |   3 +
 .../de/vipra/cmd/option/ImportCommand.java    |  20 +++
 vipra-ui/app/html/articles/show.html          |   2 +-
 .../app/html/directives/word-evolution.html   |  38 +++++
 vipra-ui/app/html/explorer.html               |  46 ++++++-
 vipra-ui/app/html/topics/show.html            |  44 +-----
 vipra-ui/app/js/controllers.js                | 130 +++++++++++-------
 vipra-ui/app/js/directives.js                 |  11 ++
 vipra-ui/app/less/app.less                    | 119 +++++++++++++---
 .../java/de/vipra/util/model/ArticleFull.java |  12 ++
 .../java/de/vipra/util/model/TopicFull.java   |   4 +-
 .../de/vipra/util/model/TopicModelFull.java   |   1 +
 .../java/de/vipra/util/model/WindowFull.java  |  85 ++++++++++++
 18 files changed, 522 insertions(+), 118 deletions(-)
 create mode 100644 vipra-backend/src/main/java/de/vipra/rest/resource/WindowResource.java
 create mode 100644 vipra-ui/app/html/directives/word-evolution.html
 create mode 100644 vipra-util/src/main/java/de/vipra/util/model/WindowFull.java

diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java
index ccc261bd..0aceea06 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java
@@ -124,12 +124,12 @@ public class TopicResource {
 				query.fields(true, StringUtils.getFields(fields));
 
 			if (fromDate != null) {
-				final Date d = new Date(fromDate * 1000);
+				final Date d = new Date(fromDate);
 				query.gte("date", d);
 			}
 
 			if (toDate != null) {
-				final Date d = new Date(toDate * 1000);
+				final Date d = new Date(toDate);
 				query.lte("date", d);
 			}
 
diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/WindowResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/WindowResource.java
new file mode 100644
index 00000000..0b697ccb
--- /dev/null
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/WindowResource.java
@@ -0,0 +1,100 @@
+package de.vipra.rest.resource;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.ServletContext;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import de.vipra.rest.Messages;
+import de.vipra.rest.model.APIError;
+import de.vipra.rest.model.ResponseWrapper;
+import de.vipra.util.Config;
+import de.vipra.util.StringUtils;
+import de.vipra.util.ex.ConfigException;
+import de.vipra.util.model.TopicModel;
+import de.vipra.util.model.WindowFull;
+import de.vipra.util.service.MongoService;
+import de.vipra.util.service.QueryBuilder;
+
+@Path("windows")
+public class WindowResource {
+
+	final MongoService<WindowFull, String> dbWindows;
+
+	public WindowResource(@Context final ServletContext servletContext) throws ConfigException, IOException {
+		final Config config = Config.getConfig();
+		dbWindows = MongoService.getDatabaseService(config, WindowFull.class);
+	}
+
+	@GET
+	@Produces(MediaType.APPLICATION_JSON)
+	public Response getWindows(@QueryParam("topicModel") final String topicModel, @QueryParam("skip") final Integer skip,
+			@QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("startDate") final String sortBy,
+			@QueryParam("fields") final String fields) {
+		final ResponseWrapper<List<WindowFull>> res = new ResponseWrapper<>();
+
+		if (res.hasErrors())
+			return res.badRequest();
+
+		try {
+			final QueryBuilder query = QueryBuilder.builder().skip(skip).limit(limit).sortBy(sortBy);
+			if (fields != null && !fields.isEmpty())
+				query.fields(true, StringUtils.getFields(fields));
+
+			if (topicModel != null && !topicModel.isEmpty())
+				query.eq("topicModel", new TopicModel(topicModel));
+
+			final List<WindowFull> windows = dbWindows.getMultiple(query);
+
+			if ((skip != null && skip > 0) || (limit != null && limit > 0))
+				res.addHeader("total", dbWindows.count(null));
+			else
+				res.addHeader("total", windows.size());
+
+			return res.ok(windows);
+		} catch (final Exception e) {
+			e.printStackTrace();
+			res.addError(new APIError(Response.Status.BAD_REQUEST, "Error", e.getMessage()));
+			return res.badRequest();
+		}
+	}
+
+	@GET
+	@Produces(MediaType.APPLICATION_JSON)
+	@Consumes(MediaType.APPLICATION_JSON)
+	@Path("{id}")
+	public Response getWindow(@PathParam("id") final String id, @QueryParam("fields") final String fields) {
+		final ResponseWrapper<WindowFull> res = new ResponseWrapper<>();
+		if (id == null) {
+			res.addError(new APIError(Response.Status.BAD_REQUEST, "ID is empty", String.format(Messages.BAD_REQUEST, "id cannot be empty")));
+			return res.badRequest();
+		}
+
+		WindowFull window;
+		try {
+			window = dbWindows.getSingle(id, StringUtils.getFields(fields));
+		} catch (final Exception e) {
+			e.printStackTrace();
+			res.addError(new APIError(Response.Status.BAD_REQUEST, "Error", e.getMessage()));
+			return res.badRequest();
+		}
+
+		if (window != null) {
+			return res.ok(window);
+		} else {
+			res.addError(new APIError(Response.Status.NOT_FOUND, "Resource not found", String.format(Messages.NOT_FOUND, "window", id)));
+			return res.notFound();
+		}
+	}
+
+}
diff --git a/vipra-cmd/runcfg/CMD.launch b/vipra-cmd/runcfg/CMD.launch
index 7b13eb9c..3f9ca734 100644
--- a/vipra-cmd/runcfg/CMD.launch
+++ b/vipra-cmd/runcfg/CMD.launch
@@ -11,7 +11,7 @@
 </listAttribute>
 <stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.m2e.launchconfig.classpathProvider"/>
 <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="de.vipra.cmd.Main"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-S test2 -M"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-D test"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="vipra-cmd"/>
 <stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.m2e.launchconfig.sourcepathProvider"/>
 <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea"/>
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/file/FilebaseWindowIndex.java b/vipra-cmd/src/main/java/de/vipra/cmd/file/FilebaseWindowIndex.java
index 375842fa..84abb8d5 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/file/FilebaseWindowIndex.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/file/FilebaseWindowIndex.java
@@ -115,12 +115,20 @@ public class FilebaseWindowIndex {
 		return windowResolution.startDate(windowDates.get(index));
 	}
 
+	public Date startDate(final Date date) {
+		return windowResolution.startDate(date);
+	}
+
 	public Date endDate(final int index) {
 		if (seqDirty)
 			resizeWindows();
 		return windowResolution.endDate(windowDates.get(index));
 	}
 
+	public Date endDate(final Date date) {
+		return windowResolution.endDate(date);
+	}
+
 	public void copy(final File modelFile) throws IOException {
 		FileUtils.copyFile(modelFile, new File(modelDir, MULT_FILE_NAME));
 	}
@@ -140,6 +148,14 @@ public class FilebaseWindowIndex {
 		return window;
 	}
 
+	public Window getWindow(final Date date) {
+		final Window window = new Window();
+		window.setStartDate(startDate(date));
+		window.setEndDate(endDate(date));
+		window.setWindowResolution(windowResolution);
+		return window;
+	}
+
 	private void resizeWindows() {
 		final List<Date> dates = new ArrayList<>(windowMap.keySet());
 		Collections.sort(dates);
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 793694a8..0c416a9a 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
@@ -371,7 +371,8 @@ public class Analyzer {
 					reducedShare += topicDistribution[idxTopic];
 					final TopicShare newTopicRef = new TopicShare();
 					final TopicFull topicFull = newTopics.get(idxTopic);
-					topicFull.setArticlesCount(topicFull.getArticlesCount() + 1);
+					Integer articlesCount = topicFull.getArticlesCount();
+					topicFull.setArticlesCount(articlesCount == null ? 1 : articlesCount + 1);
 					newTopicRef.setTopic(new Topic(topicFull.getId()));
 					newTopicRef.setShare(topicDistribution[idxTopic]);
 					newTopicRefs.add(newTopicRef);
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/DeleteModelCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/DeleteModelCommand.java
index d1434b08..483c320f 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/DeleteModelCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/DeleteModelCommand.java
@@ -12,6 +12,7 @@ import de.vipra.util.model.SequenceFull;
 import de.vipra.util.model.TextEntityFull;
 import de.vipra.util.model.TopicFull;
 import de.vipra.util.model.TopicModel;
+import de.vipra.util.model.WindowFull;
 import de.vipra.util.model.WordFull;
 import de.vipra.util.service.MongoService;
 import de.vipra.util.service.QueryBuilder;
@@ -33,6 +34,7 @@ public class DeleteModelCommand implements Command {
 		final MongoService<SequenceFull, ObjectId> dbSequences = MongoService.getDatabaseService(config, SequenceFull.class);
 		final MongoService<WordFull, String> dbWords = MongoService.getDatabaseService(config, WordFull.class);
 		final MongoService<TextEntityFull, String> dbEntities = MongoService.getDatabaseService(config, TextEntityFull.class);
+		final MongoService<WindowFull, String> dbWindows = MongoService.getDatabaseService(config, WindowFull.class);
 
 		for (final String name : names) {
 			final File modelDir = new File(config.getDataDirectory(), name);
@@ -51,6 +53,7 @@ public class DeleteModelCommand implements Command {
 			dbSequences.deleteMultiple(builder);
 			dbWords.deleteMultiple(builder);
 			dbEntities.deleteMultiple(builder);
+			dbWindows.deleteMultiple(builder);
 		}
 	}
 
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java
index e3f83fa4..53f1ac3a 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java
@@ -20,6 +20,7 @@ import org.json.simple.parser.ParseException;
 
 import de.vipra.cmd.file.Filebase;
 import de.vipra.cmd.file.FilebaseException;
+import de.vipra.cmd.file.FilebaseWindowIndex;
 import de.vipra.cmd.file.FilebaseWordIndex;
 import de.vipra.cmd.text.ProcessedText;
 import de.vipra.cmd.text.Processor;
@@ -41,6 +42,8 @@ import de.vipra.util.model.TextEntityFull;
 import de.vipra.util.model.TopicModel;
 import de.vipra.util.model.TopicModelConfig;
 import de.vipra.util.model.TopicModelFull;
+import de.vipra.util.model.Window;
+import de.vipra.util.model.WindowFull;
 import de.vipra.util.model.WordFull;
 import de.vipra.util.service.MongoService;
 
@@ -78,9 +81,11 @@ public class ImportCommand implements Command {
 	private MongoService<TopicModelFull, String> dbTopicModels;
 	private MongoService<WordFull, String> dbWords;
 	private MongoService<TextEntityFull, String> dbEntities;
+	private MongoService<WindowFull, String> dbWindows;
 	private TopicModelConfig modelConfig;
 	private SpotlightAnalyzer spotlightAnalyzer;
 	private Filebase filebase;
+	private FilebaseWindowIndex windowIndex;
 	private Processor processor;
 	private ArticleBuffer buffer;
 	private TopicModelFull topicModel;
@@ -188,6 +193,7 @@ public class ImportCommand implements Command {
 				article.setProcessedText(processedText.getWords());
 				article.setWords(processedText.getArticleWords());
 				article.setTopicModel(new TopicModel(topicModel.getId()));
+				article.setWindow(windowIndex.getWindow(article.getDate()));
 
 				// generate article stats
 				final ArticleStats stats = new ArticleStats();
@@ -267,6 +273,7 @@ public class ImportCommand implements Command {
 
 		buffer = new ArticleBuffer(dbArticles);
 		filebase = new Filebase(modelConfig, config.getDataDirectory());
+		windowIndex = filebase.getWindowIndex();
 		topicModel = new TopicModelFull(modelConfig.getName(), modelConfig);
 		newTextEntities = new HashSet<>();
 
@@ -310,6 +317,18 @@ public class ImportCommand implements Command {
 		 */
 		dbEntities.createMultiple(newTextEntities);
 
+		/*
+		 * add new windows
+		 */
+		final List<Window> windows = filebase.getWindowIndex().getWindows();
+		final List<WindowFull> newWindows = new ArrayList<>(windows.size());
+		for (final Window window : windows) {
+			final WindowFull newWindow = new WindowFull(window);
+			newWindow.setTopicModel(topicModelRef);
+			newWindows.add(newWindow);
+		}
+		dbWindows.createMultiple(newWindows);
+
 		/*
 		 * run information
 		 */
@@ -323,6 +342,7 @@ public class ImportCommand implements Command {
 		dbTopicModels = MongoService.getDatabaseService(config, TopicModelFull.class);
 		dbEntities = MongoService.getDatabaseService(config, TextEntityFull.class);
 		dbWords = MongoService.getDatabaseService(config, WordFull.class);
+		dbWindows = MongoService.getDatabaseService(config, WindowFull.class);
 		processor = new Processor();
 		for (final TopicModelConfig modelConfig : config.getTopicModelConfigs(models)) {
 			importForModel(modelConfig);
diff --git a/vipra-ui/app/html/articles/show.html b/vipra-ui/app/html/articles/show.html
index 3cb8fd1f..a80b75d0 100644
--- a/vipra-ui/app/html/articles/show.html
+++ b/vipra-ui/app/html/articles/show.html
@@ -98,7 +98,7 @@
             </tr>
           </tbody>
         </table>
-        <p class="text-muted" ng-hide="article.similarArticles.length">No similar articles.</p>
+        <p class="text-muted" ng-hide="article.similarArticles.length">No similar articles</p>
         <hr>
         <div class="text-justify" ng-bind-html="::article.text"></div>
       </div>
diff --git a/vipra-ui/app/html/directives/word-evolution.html b/vipra-ui/app/html/directives/word-evolution.html
new file mode 100644
index 00000000..6dcd1910
--- /dev/null
+++ b/vipra-ui/app/html/directives/word-evolution.html
@@ -0,0 +1,38 @@
+<div class="word-evolution panel panel-default">
+  <div class="topbar">
+    <small>Values:</small>
+    <div class="btn-group">
+      <a class="btn btn-sm btn-default" ng-model="wordSeqstyle" bs-radio="'absolute'">Absolute</a>
+      <a class="btn btn-sm btn-default" ng-model="wordSeqstyle" bs-radio="'relative'">Relative</a>
+    </div>
+    &nbsp;
+    <small>Chart:</small>
+    <div class="btn-group">
+      <a class="btn btn-sm btn-default" ng-model="wordChartstyle" bs-radio="'areaspline'">Area</a>
+      <a class="btn btn-sm btn-default" ng-model="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" ng-cloak>Reset zoom</a>
+    </div>
+  </div>
+  <div class="panel-body">
+    <div class="topic-list sidebar">
+      <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.id}}" ng-change="redrawWordEvolutionChart()">
+            <label class="check" ng-attr-for="{{::word.id}}">
+              <word-link word="::word" />
+            </label>
+          </div>
+        </li>
+      </ul>
+    </div>
+    <div class="center message-container">
+      <div class="wrapper">
+        <div class="chart area-chart" ng-attr-id="{{chartId}}" highcharts="topicWord"></div>
+      </div>
+    </div>
+  </div>
+  <div class="message text-muted" ng-show="!topic">No topic selected</div>
+</div>
\ No newline at end of file
diff --git a/vipra-ui/app/html/explorer.html b/vipra-ui/app/html/explorer.html
index e9c7bbf5..cac01643 100644
--- a/vipra-ui/app/html/explorer.html
+++ b/vipra-ui/app/html/explorer.html
@@ -92,10 +92,48 @@
     </div>
     <div class="center">
       <div class="wrapper">
-        <div class="topbar">
-          <small>Sequence:</small>
-          <sequence-dropdown ng-model="explorerModels.activeSequence" sequences="explorerModels.activeTopic.sequences"></sequence-dropdown>
-          <button class="btn btn-sm btn-default" ng-click="clearSequence()" ng-disabled="!explorerModels.activeSequence">Clear</button>
+        <div class="topbar tabs">
+          <ul class="nav nav-tabs" role="tablist">
+            <li class="active">
+              <a data-target=".tab-evolution" data-toggle="tab" bs-tab>Word Evolution</a>
+            </li>
+            <li>
+              <a data-target=".tab-articles" data-toggle="tab" bs-tab>Articles</a>
+            </li>
+          </ul>
+        </div>
+        <div class="tab-content fullsize">
+          <div role="tabpanel" class="tab-pane active tab-evolution">
+            <word-evolution topic="explorerModels.activeTopic"/>
+          </div>
+          <div role="tabpanel" class="tab-pane active tab-articles auto-overflow">
+            <div class="panel panel-default">
+              <div class="topbar">
+                <small>Sequence:</small>
+                <sequence-dropdown ng-model="explorerModels.activeSequence" sequences="explorerModels.activeTopic.sequences"></sequence-dropdown>
+                <button class="btn btn-sm btn-default" ng-click="clearSequence()" ng-disabled="!explorerModels.activeSequence">Clear</button>
+              </div>
+              <table class="table table-bordered table-condensed table-fixed">
+                <thead>
+                  <tr>
+                    <th ng-model="explorerModels.articlesSort" sort-by="article.title">Article</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr ng-repeat="article in articles | orderBy:explorerModels.articlesSort">
+                    <td>
+                      <article-link article="::article" />
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+              <div class="panel-footer">
+                <ng-pluralize count="articles.length" when="{0:'No articles',1:'First entity',other:'First {} articles'}"></ng-pluralize>
+                <button class="btn btn-default btn-sm" ng-click="showMoreArticles()" ng-show="articles.length<allArticles.length" ng-cloak>Show more</button>
+                <button class="btn btn-default btn-sm" ng-click="showAllArticles()" ng-show="articles.length<allArticles.length" ng-cloak>Show all</button>
+              </div>
+            </div>
+          </div>
         </div>
       </div>
     </div>
diff --git a/vipra-ui/app/html/topics/show.html b/vipra-ui/app/html/topics/show.html
index 9b10d2f1..9a0b3e68 100644
--- a/vipra-ui/app/html/topics/show.html
+++ b/vipra-ui/app/html/topics/show.html
@@ -1,4 +1,4 @@
-<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'topics.show'">
+<div class="container topic-show" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'topics.show'">
   <div class="page-header no-border">
     <span class="label label-default">Topic</span>
     <h1>
@@ -45,7 +45,7 @@
     </ul>
     <div class="tab-content">
       <div role="tabpanel" class="tab-pane active tab-info">
-        <h3>Relevance</h3>
+        <h3>Relevance <info text="Topic relevance: topic distribution sum divided by number of articles in a sequence"/></h3>
         <div class="panel panel-default">
           <div class="panel-heading">
             <small>Values:</small>
@@ -67,44 +67,8 @@
             <div class="chart area-chart" id="topicRelChart" highcharts="topicSeq"></div>
           </div>
         </div>
-        <h3>Word evolution</h3>
-        <div class="panel panel-default">
-          <div class="panel-heading">
-            <small>Values:</small>
-            <div class="btn-group">
-              <a class="btn btn-sm btn-default" ng-model="topicsShowModels.wordSeqstyle" bs-radio="'absolute'">Absolute</a>
-              <a class="btn btn-sm btn-default" ng-model="topicsShowModels.wordSeqstyle" bs-radio="'relative'">Relative</a>
-            </div>
-            &nbsp;
-            <small>Chart:</small>
-            <div class="btn-group">
-              <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" ng-cloak>Reset zoom</a>
-            </div>
-          </div>
-          <div class="panel-body">
-            <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.id}}" ng-change="redrawWordEvolutionChart()">
-                      <label class="check" ng-attr-for="{{::word.id}}">
-                        <word-link 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>
-            </div>
-          </div>
-        </div>
+        <h3>Word evolution <info text="Word evolution: absolute word probability per sequence"/></h3>
+        <word-evolution topic="topic"/>
       </div>
       <div role="tabpanel" class="tab-pane tab-sequences">
         <h3>Sequences</h3>
diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js
index c6643bea..cf0929cc 100644
--- a/vipra-ui/app/js/controllers.js
+++ b/vipra-ui/app/js/controllers.js
@@ -534,7 +534,7 @@
         if (!$scope.rootModels.topicModel) return;
 
         TopicFactory.query({
-          fields: 'name,sequences,avgRelevance,varRelevance,risingRelevance,fallingRelevance,risingDecayRelevance',
+          fields: 'name,sequences,avgRelevance,varRelevance,risingRelevance,fallingRelevance,risingDecayRelevance,words',
           topicModel: $scope.rootModels.topicModel.id
         }, function(data) {
           $scope.topics = data;
@@ -612,7 +612,7 @@
         }
 
         // highcharts configuration
-        $scope.topicSeq = areaRelevanceChart(series, $scope.explorerModels.chartstyle,
+        $scope.topicSeq = areaRelevanceChart(series, 'Topic Relevance', $scope.explorerModels.chartstyle,
           $scope.explorerModels.chartstack, $scope.pointSelected);
         $scope.topicsSelected = series.length;
       };
@@ -682,6 +682,29 @@
         $scope.explorerModels.activeSequence = seq;
       };
 
+      $scope.clearSequence = function() {
+        if(!$scope.explorerModels.activeSequence) return;
+
+        delete $scope.explorerModels.activeSequence;
+        delete $scope.articles;
+        var selectedPoints = topicRelChartElement.highcharts().getSelectedPoints();
+        for(var i = 0; i < selectedPoints.length; i++) {
+          selectedPoints[i].select(false);
+        }
+      };
+
+      $scope.sequenceChanged = function() {
+        if(!$scope.explorerModels.activeTopic || !$scope.explorerModels.activeSequence) return;
+
+        TopicFactory.articles({
+          id: $scope.explorerModels.activeTopic.id,
+          from: new Date($scope.explorerModels.activeSequence.window.startDate).getTime(),
+          to: new Date($scope.explorerModels.activeSequence.window.endDate).getTime()
+        }, function(data) {
+          $scope.articles = data;
+        });
+      };
+
       $scope.$watchGroup(['explorerModels.seqstyle', 'explorerModels.chartstyle', 'explorerModels.chartstack'], $scope.redrawGraph);
 
       $scope.$watch('explorerModels.sorttopics', function() {
@@ -699,15 +722,19 @@
         }, 0);
       });
 
-      $scope.clearSequence = function() {
-        if(!$scope.explorerModels.activeSequence) return;
+      $scope.$watch('explorerModels.activeTopic', function() {
+        if(!$scope.explorerModels.activeTopic) return;
 
-        delete $scope.explorerModels.activeSequence;
-        var selectedPoints = topicRelChartElement.highcharts().getSelectedPoints();
-        for(var i = 0; i < selectedPoints.length; i++) {
-          selectedPoints[i].select(false);
+        // preselect some words
+        if ($scope.explorerModels.activeTopic.words) {
+          for (var i = 0; i < Math.min(3, $scope.explorerModels.activeTopic.words.length); i++)
+            $scope.explorerModels.activeTopic.words[i].selected = true;
         }
-      };
+      });
+
+      $scope.$watch('explorerModels.activeSequence', function() {
+        $scope.sequenceChanged();
+      });
     }
   ]);
 
@@ -958,7 +985,7 @@
 
         $timeout(function() {
           $scope.redrawRelevanceGraph();
-          $scope.redrawWordEvolutionChart();
+          //$scope.redrawWordEvolutionChart(); TODO remove
         }, 0);
       });
 
@@ -977,30 +1004,7 @@
         $scope.topicSeq = areaRelevanceChart([{
           name: $scope.topic.name,
           data: relevances
-        }], $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.id,
-            data: probs
-          });
-        }
-
-        $scope.topicWord = areaRelevanceChart(evolutions, $scope.topicsShowModels.wordChartstyle);
-        $scope.wordsSelected = evolutions.length;
+        }], 'Topic Relevance', $scope.topicsShowModels.relChartstyle);
       };
 
       var topicRelChartElement = $('#topicRelChart');
@@ -1012,15 +1016,6 @@
         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;
@@ -1085,9 +1080,6 @@
       $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.$watch('topicsShowModels.sequence', function() {
         if (!$scope.topicsShowModels.sequence) return;
 
@@ -1462,11 +1454,53 @@
     }
   ]);
 
+  app.controller('WordEvolutionController', ['$scope',
+    function($scope) {
+
+      $scope.chartId = Vipra.randomId();
+      $scope.wordSeqstyle = 'absolute';
+      $scope.wordChartstyle = 'spline';
+
+      $scope.resetWordZoom = function() {
+        if (!$scope.wordsSelected) return;
+        var highcharts = $('#' + $scope.chartId).highcharts();
+        if (!highcharts) return;
+
+        highcharts.xAxis[0].setExtremes(null, null);
+      };
+
+      $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.wordSeqstyle === 'relative' ? word.sequenceProbabilitiesChange[j] : word.sequenceProbabilities[j];
+            probs.push([new Date($scope.topic.sequences[j].window.startDate).getTime(), prob]);
+          }
+          evolutions.push({
+            name: word.id,
+            data: probs
+          });
+        }
+
+        $scope.topicWord = areaRelevanceChart(evolutions, 'Word Evolution', $scope.wordChartstyle);
+        $scope.wordsSelected = evolutions.length;
+      };
+
+      $scope.$watchGroup(['wordSeqstyle', 'wordChartstyle', 'topic'], $scope.redrawWordEvolutionChart);
+    }
+  ]);
+
   /****************************************************************************
    * Shared Highcharts configurations
    ****************************************************************************/
 
-  function areaRelevanceChart(series, chartType, chartStack, clickCallback) {
+  function areaRelevanceChart(series, title, chartType, chartStack, clickCallback) {
     return {
       chart: {
         type: (chartType || 'areaspline'),
@@ -1475,7 +1509,7 @@
         spacingRight: 0
       },
       title: {
-        text: 'Topic Relevance'
+        text: title
       },
       xAxis: {
         type: 'datetime',
diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js
index 10007e5e..6854967f 100644
--- a/vipra-ui/app/js/directives.js
+++ b/vipra-ui/app/js/directives.js
@@ -464,4 +464,15 @@
     };
   }]);
 
+  app.directive('wordEvolution', [function() {
+    return {
+      scope: {
+        topic: '='
+      },
+      replace: true,
+      controller: 'WordEvolutionController',
+      templateUrl: 'html/directives/word-evolution.html'
+    };
+  }]);
+
 })();
\ No newline at end of file
diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less
index acaf5cef..b9408ef9 100644
--- a/vipra-ui/app/less/app.less
+++ b/vipra-ui/app/less/app.less
@@ -236,11 +236,12 @@ a:hover {
 
 .colorbox {
   display: inline-block;
-  width: 10px;
+  width: 5px;
   height: 21px;
   vertical-align: middle;
   float: right;
   border-radius: 3px;
+  padding-left: 5px;
 }
 
 .valuebar {
@@ -265,36 +266,64 @@ a:hover {
   padding: 5px;
   vertical-align: middle;
   margin-bottom: 10px;
-}
+  border-bottom: 1px solid #ddd;
+  height: 41px;
 
-.explorer {
-  @sidebar-padding: 5px;
-  @sidebar-width: 250px;
-  .sidebar {
-    background: #f9f9f9;
-    padding: @sidebar-padding;
-    height: 100%;
-    position: absolute;
-    width: @sidebar-width;
-    z-index: 1;
-    > * + * {
-      margin-top: 5px;
+  &.tabs {
+    padding: 5px 0 0 0;
+    border-bottom: 0;
+  }
+
+  > .nav-tabs {
+    padding-left: 5px;
+    > li > a {
+      padding: 7px 15px;
     }
   }
-  .center {
-    padding: 0 0 0 @sidebar-width;
-    height: 100%;
-    width: 100%;
+
+  & + .fullsize {
+    position: absolute;
+    top: 41px;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    padding: 5px;
   }
+}
+
+@sidebar-padding: 5px;
+@sidebar-width: 250px;
+.sidebar {
+  background: #f9f9f9;
+  padding: @sidebar-padding;
+  height: 100%;
+  position: absolute;
+  width: @sidebar-width;
+  z-index: 1;
+  overflow: auto;
+  > * + * {
+    margin-top: 5px;
+  }
+}
+.center {
+  padding: 0 0 0 @sidebar-width;
+  height: 100%;
+  width: 100%;
+}
+
+.explorer {
   .chart {
     padding: 15px;
     position: absolute;
-    top: 35px;
+    top: 0px;
     left: 0;
     right: 0;
     bottom: 0;
     height: auto !important;
   }
+  #topicRelChart {
+    top: 41px;
+  }
   .sequence {
     flex: 1 0 0;
   }
@@ -352,6 +381,9 @@ a:hover {
   .colorbox.shown {
     visibility: visible;
   }
+  .panel {
+    margin: 0;
+  }
 }
 
 .radio-inline {
@@ -384,6 +416,7 @@ a:hover {
 .chart,
 .highcharts-container {
   width: 100% !important;
+  height: 100% !important;
 }
 
 .percent-align {
@@ -589,6 +622,54 @@ entity-menu {
   font-size: 14px;
 }
 
+.text-tab {
+  padding: 0 5px 5px 5px;
+}
+
+.tab-evolution,
+.tab-articles {
+  height: 100%;
+}
+
+.word-evolution {
+  height: 100%;
+  margin: 0;
+  position: relative;
+  > .panel-body {
+    position: absolute;
+    top: 41px;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    padding: 0;
+    > .row {
+      height: 100%;
+      > .topic-list {
+        overflow: auto;
+      }
+    }
+  }
+}
+
+.topic-show .tab-info {
+  .panel {
+    height: 500px;
+  }
+  .wrapper {
+    padding: 15px;
+  }
+}
+
+.panel {
+  .topbar {
+    margin: 0;
+  }
+}
+
+.auto-overflow {
+  overflow: auto;
+}
+
 @-moz-keyframes spin {
   100% {
     -moz-transform: rotateY(360deg);
diff --git a/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
index cebad677..d9929868 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
@@ -54,6 +54,10 @@ public class ArticleFull implements Model<ObjectId>, Serializable {
 	@ElasticIndex("date")
 	private Date date;
 
+	@QueryIgnore(multi = true)
+	@Embedded
+	private Window window;
+
 	@Reference
 	@QueryIgnore(multi = true)
 	private TopicModel topicModel;
@@ -155,6 +159,14 @@ public class ArticleFull implements Model<ObjectId>, Serializable {
 		this.date = date;
 	}
 
+	public Window getWindow() {
+		return window;
+	}
+
+	public void setWindow(Window window) {
+		this.window = window;
+	}
+
 	public void setDate(final String date) {
 		final SimpleDateFormat df = new SimpleDateFormat(Constants.DATETIME_FORMAT);
 		try {
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java b/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java
index 9bd32d02..6424111e 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java
@@ -162,11 +162,11 @@ public class TopicFull implements Model<ObjectId>, Serializable {
 		this.risingDecayRelevance = risingDecayRelevance;
 	}
 
-	public int getArticlesCount() {
+	public Integer getArticlesCount() {
 		return articlesCount;
 	}
 
-	public void setArticlesCount(final int articlesCount) {
+	public void setArticlesCount(final Integer articlesCount) {
 		this.articlesCount = articlesCount;
 	}
 
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicModelFull.java b/vipra-util/src/main/java/de/vipra/util/model/TopicModelFull.java
index 7bd55e67..c5aba279 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TopicModelFull.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TopicModelFull.java
@@ -33,6 +33,7 @@ public class TopicModelFull implements Model<String>, Comparable<TopicModelFull>
 
 	private Date lastIndexed;
 
+	@Embedded
 	@QueryIgnore(multi = true)
 	private List<Window> windows;
 
diff --git a/vipra-util/src/main/java/de/vipra/util/model/WindowFull.java b/vipra-util/src/main/java/de/vipra/util/model/WindowFull.java
new file mode 100644
index 00000000..738b7517
--- /dev/null
+++ b/vipra-util/src/main/java/de/vipra/util/model/WindowFull.java
@@ -0,0 +1,85 @@
+package de.vipra.util.model;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import org.mongodb.morphia.annotations.Entity;
+import org.mongodb.morphia.annotations.Id;
+import org.mongodb.morphia.annotations.Reference;
+
+import de.vipra.util.Constants.WindowResolution;
+import de.vipra.util.an.QueryIgnore;
+
+@SuppressWarnings("serial")
+@Entity(value = "windows", noClassnameStored = true)
+public class WindowFull implements Model<String>, Serializable, Comparable<WindowFull> {
+
+	@Id
+	private String id;
+
+	private Date startDate;
+
+	private Date endDate;
+
+	private WindowResolution windowResolution;
+
+	@QueryIgnore(multi = true)
+	@Reference
+	private TopicModel topicModel;
+
+	public WindowFull() {}
+
+	public WindowFull(Window window) {
+		this.id = Long.toString(window.getStartDate().getTime());
+		this.startDate = window.getStartDate();
+		this.endDate = window.getEndDate();
+		this.windowResolution = window.getWindowResolution();
+	}
+
+	@Override
+	public String getId() {
+		return id;
+	}
+
+	@Override
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public Date getStartDate() {
+		return startDate;
+	}
+
+	public void setStartDate(Date startDate) {
+		this.startDate = startDate;
+	}
+
+	public Date getEndDate() {
+		return endDate;
+	}
+
+	public void setEndDate(Date endDate) {
+		this.endDate = endDate;
+	}
+
+	public WindowResolution getWindowResolution() {
+		return windowResolution;
+	}
+
+	public void setWindowResolution(WindowResolution windowResolution) {
+		this.windowResolution = windowResolution;
+	}
+
+	public TopicModel getTopicModel() {
+		return topicModel;
+	}
+
+	public void setTopicModel(TopicModel topicModel) {
+		this.topicModel = topicModel;
+	}
+
+	@Override
+	public int compareTo(WindowFull o) {
+		return startDate.compareTo(o.getStartDate());
+	}
+}
-- 
GitLab