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> + + <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> - - <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