diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java index 818444c17302b7adfdd8c847dcceb14270bb8055..97182d3ed141443e085d8360d69ba8e8da7cbfb7 100644 --- a/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java +++ b/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java @@ -74,8 +74,9 @@ public class InfoResource { info.put("const.topicautoname", Constants.TOPIC_AUTO_NAMING_WORDS); info.put("const.ktopics", Constants.K_TOPICS); info.put("const.ktopicwords", Constants.K_TOPIC_WORDS); + info.put("const.decaylambda", Constants.RISING_DECAY_LAMBDA); info.put("const.minrelprob", Constants.MINIMUM_RELATIVE_PROB); - info.put("constminshare", Constants.MINIMUM_SHARE); + info.put("const.minshare", Constants.MINIMUM_SHARE); info.put("const.dynminiter", Constants.DYNAMIC_MIN_ITER); info.put("const.dynmaxiter", Constants.DYNAMIC_MAX_ITER); info.put("const.statiter", Constants.STATIC_ITER); diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/lda/DTMAnalyzer.java b/vipra-cmd/src/main/java/de/vipra/cmd/lda/DTMAnalyzer.java index 9287f12beb9aeeaa452fced3ecc8723d82eb0dce..20ca5d260dd73809c275033cbb4723d10fcbf9aa 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/lda/DTMAnalyzer.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/lda/DTMAnalyzer.java @@ -272,9 +272,19 @@ public class DTMAnalyzer extends Analyzer { newSequences.add(newSequenceFull); newTopicSequences.add(new Sequence(newSequenceFull.getId())); + // sequence offset is current position in list of sequences + // of this topic sequenceOffset += sequenceSize; + + // relevance is summed up to calculate average later on relevanceSum += relevance; + + // relevances are gathered to calculate variance and + // rising/falling relevance relevances[idxSeq] = relevance; + + // previous relevance is remembered to calculate difference + // to next relevance prevRelevance = relevance; } @@ -292,6 +302,25 @@ public class DTMAnalyzer extends Analyzer { for (final double relevance : relevances) variance += Math.pow(relevance - average, 2); newTopic.setVarRelevance(variance / sequencesCount); + + // calculate rising/falling/rising-decay relevances + double risingRelevance = 0; + double fallingRelevance = 0; + double risingDecayRelevance = 0; + prevRelevance = relevances[0]; + for (int idxSeq2 = 1; idxSeq2 < relevances.length; idxSeq2++) { + double relevanceDiff = relevances[idxSeq2] - prevRelevance; + if (relevanceDiff > 0) { + risingRelevance += relevanceDiff; + } else { + fallingRelevance += Math.abs(relevanceDiff); + } + risingDecayRelevance += Math.exp(-Constants.RISING_DECAY_LAMBDA * (sequencesCount - idxSeq2 + 1)) + * relevanceDiff; + } + newTopic.setRisingRelevance(risingRelevance); + newTopic.setFallingRelevance(fallingRelevance); + newTopic.setRisingDecayRelevance(risingDecayRelevance); } // recreate topics and words diff --git a/vipra-ui/app/html/about.html b/vipra-ui/app/html/about.html index bc1175ccd739d7b1d4ef483d20d2c69923e313b8..c1849104efce06730ac1e435ce058ba21eefd188 100644 --- a/vipra-ui/app/html/about.html +++ b/vipra-ui/app/html/about.html @@ -147,6 +147,15 @@ The maximum number of words that are associated to a single topic. </td> </tr> + <tr> + <th>Rising decay weight</th> + <td ng-bind-template="{{::info.const.decaylambda}}"></td> + </tr> + <tr class="well"> + <td colspan="2"> + A weight to the rising decay caulculation of topic relevances. The higher this value, the more focus is put on later sequences containing more recent documents. + </td> + </tr> <tr> <th>Minimum relative probability</th> <td ng-bind-template="{{::info.const.minrelprob}}"></td> diff --git a/vipra-ui/app/html/explorer.html b/vipra-ui/app/html/explorer.html index 8d9c2636399ad4bee3640f4251b86d891ad61fb2..dca997dde3e850eaa5bcf5686cff12d3ad3a2f58 100644 --- a/vipra-ui/app/html/explorer.html +++ b/vipra-ui/app/html/explorer.html @@ -9,9 +9,9 @@ <a class="btn btn-sm btn-default" ng-model="opts.sorttopics" bs-radio="'name'" title="Sort by name">α</a> <a class="btn btn-sm btn-default" ng-model="opts.sorttopics" bs-radio="'avgRelevance'" title="Sort by relevance average">μ</a> <a class="btn btn-sm btn-default" ng-model="opts.sorttopics" bs-radio="'varRelevance'" title="Sort by relevance variance">σ</a> - <a class="btn btn-sm btn-default" ng-model="opts.sorttopics" bs-radio="'frel'" title="Sort by falling relevance">↘</a> - <a class="btn btn-sm btn-default" ng-model="opts.sorttopics" bs-radio="'rrel'" title="Sort by rising relevance">↗</a> - <a class="btn btn-sm btn-default" ng-model="opts.sorttopics" bs-radio="'rreld'" title="Sort by rising relevance with decay">↝</a> + <a class="btn btn-sm btn-default" ng-model="opts.sorttopics" bs-radio="'fallingRelevance'" title="Sort by falling relevance">↘</a> + <a class="btn btn-sm btn-default" ng-model="opts.sorttopics" bs-radio="'risingRelevance'" title="Sort by rising relevance">↗</a> + <a class="btn btn-sm btn-default" ng-model="opts.sorttopics" bs-radio="'risingDecayRelevance'" title="Sort by rising relevance with decay">↝</a> </div> <div class="btn-group btn-group-justified"> <input type="text" class="form-control" ng-model="search.$" placeholder="Filter"> @@ -29,20 +29,28 @@ </li> </ul> </div> - <div class="chart message-container"> + <div class="center message-container"> <div class="topbar"> + <small>Values:</small> <div class="btn-group"> - <span class="btn btn-sm btn-default-none">Values:</span> <a class="btn btn-sm btn-default" ng-model="opts.seqstyle" bs-radio="'absolute'">Absolute</a> <a class="btn btn-sm btn-default" ng-model="opts.seqstyle" bs-radio="'relative'">Relative</a> </div> + + <small>Chart:</small> <div class="btn-group"> - <span class="btn btn-sm btn-default-none">Chart:</span> <a class="btn btn-sm btn-default" ng-model="opts.chartstyle" bs-radio="'areaspline'">Area</a> <a class="btn btn-sm btn-default" ng-model="opts.chartstyle" bs-radio="'spline'">Line</a> </div> + + <small>Stacking:</small> + <div class="btn-group"> + <a class="btn btn-sm btn-default" ng-model="opts.chartstack" bs-radio="'none'">None</a> + <a class="btn btn-sm btn-default" ng-model="opts.chartstack" bs-radio="'normal'">Value</a> + <a class="btn btn-sm btn-default" ng-model="opts.chartstack" bs-radio="'percent'">Percent</a> + </div> </div> - <div class="area-chart" id="topic-seq" highcharts="topicSeq"></div> - <div class="message" ng-show="!topicsSelected">No topic selected</div> + <div class="chart" id="topic-seq" highcharts="topicSeq"></div> + <div class="message text-muted" ng-show="!topicsSelected">No topic selected</div> </div> </div> \ No newline at end of file diff --git a/vipra-ui/app/html/index.html b/vipra-ui/app/html/index.html index 5798ff160834e05c92b0b70edcaeac3984d94aee..e672fa4cab3518df7d542c772e10b02bb3c41783 100644 --- a/vipra-ui/app/html/index.html +++ b/vipra-ui/app/html/index.html @@ -43,7 +43,7 @@ <h4>Results</h4> <ul class="list-unstyled search-results"> <li class="search-result" ng-repeat="article in searchResults"> - <a ui-sref="{{::articles.show}}({id:article.id})" ng-bind="article.title"></a> + <a ui-sref="articles.show({id:article.id})" ng-bind="article.title"></a> <p> <span class="text" ng-bind="article.text"></span> <br> diff --git a/vipra-ui/app/index.html b/vipra-ui/app/index.html index 002b44c155d9b120542d7e82ebe174b131e0ac72..a9aa98dfc149016475c2610748884a05ac9f2d71 100644 --- a/vipra-ui/app/index.html +++ b/vipra-ui/app/index.html @@ -67,9 +67,9 @@ </li> </ul> <ul class="nav navbar-nav navbar-right"> - <li ng-class="{active:$state.includes('about')}"> + <li ui-sref-active="active"> <a ui-sref="about"> - <span class="glyphicon glyphicon-question-sign"></span> + About </a> </li> </ul> diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js index e1f8a69a655b694e3b9349616a9c3cfd34a1df86..4b791524bc1b5836aaecfb4f73372957fde61e7d 100644 --- a/vipra-ui/app/js/controllers.js +++ b/vipra-ui/app/js/controllers.js @@ -301,11 +301,12 @@ $scope.opts = { sorttopics: 'name', seqstyle: 'absolute', - chartstyle: 'areaspline' + chartstyle: 'areaspline', + chartstack: 'none' }; TopicFactory.query({ - fields: 'name,sequences,avgRelevance,varRelevance' + fields: 'name,sequences,avgRelevance,varRelevance,risingRelevance,fallingRelevance,risingDecayRelevance' }, function(data) { $scope.topics = data; var colors = Vipra.generateColors($scope.topics.length); @@ -336,8 +337,6 @@ for (var i = 0; i < $scope.topics.length; i++) { if ($scope.opts.selectedTopics[$scope.topics[i].id]) { var topic = $scope.topics[i], - min = 0, - max = 0, relevances = []; // data array with relevances and min/max @@ -347,19 +346,10 @@ relevances.push({ x: new Date(sequence.window.startDate).getTime(), y: relevance, - id: topic.id, seq: sequence }); - if (relevances[j].y > relevances[max].y) - max = j; - else if (relevances[j].y < relevances[min].y) - min = j; } - // mark maximum and minimum value - relevances[max].marker = { symbol: 'triangle', fillColor: '#f00' }; - relevances[min].marker = { symbol: 'triangle-down', fillColor: '#f00' }; - series.push({ name: topic.name, data: relevances, @@ -369,7 +359,7 @@ } // highcharts configuration - $scope.topicSeq = areaRelevanceChart(series, $scope.selectSequence, $scope.opts.chartstyle); + $scope.topicSeq = areaRelevanceChart(series, $scope.selectSequence, $scope.opts.chartstyle, $scope.opts.chartstack); $scope.topicsSelected = series.length; }; @@ -380,7 +370,7 @@ }; $scope.$watchCollection('opts.selectedTopics', $scope.redrawGraph); - $scope.$watchGroup(['opts.seqstyle', 'opts.chartstyle'], $scope.redrawGraph); + $scope.$watchGroup(['opts.seqstyle', 'opts.chartstyle', 'opts.chartstack'], $scope.redrawGraph); } ]); @@ -531,33 +521,15 @@ $scope.$watch('opts.seqstyle', function(style) { if (!style || !$scope.topic || !$scope.topic.sequences) return; console.log('redraw relevance chart'); - var min = 0, - max = 0, - relevances = []; + var relevances = []; // create series for (var i = 0, sequence, relevance; i < $scope.topic.sequences.length; i++) { sequence = $scope.topic.sequences[i]; relevance = $scope.opts.seqstyle === 'relative' ? sequence.relevanceChange : sequence.relevance; relevances.push([new Date(sequence.window.startDate).getTime(), relevance]); - if (relevances[i][1] > relevances[max][1]) - max = i; - else if (relevances[i][1] < relevances[min][1]) - min = i; } - // mark maximum and minimum value - relevances[max] = { - x: relevances[max][0], - y: relevances[max][1], - marker: { symbol: 'triangle', fillColor: '#f00' } - }; - relevances[min] = { - x: relevances[min][0], - y: relevances[min][1], - marker: { symbol: 'triangle-down', fillColor: '#f00' } - }; - // highcharts configuration $scope.topicSeq = areaRelevanceChart([{ name: $scope.topic.name, data: relevances }]); }); @@ -680,7 +652,7 @@ * Shared Highcharts configurations ****************************************************************************/ - function areaRelevanceChart(series, clickCallback, chartType) { + function areaRelevanceChart(series, clickCallback, chartType, chartStack) { return { chart: { type: (chartType || 'areaspline'), @@ -701,8 +673,12 @@ legend: { enabled: false }, credits: { enabled: false }, plotOptions: { - areaspline: { fillOpacity: 0.5 }, + areaspline: { + fillOpacity: 0.5, + }, series: { + stacking: (chartStack === 'none' ? null : chartStack), + animation: false, cursor: 'pointer', allowPointSelect: true, point: { diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less index 85fbef43bfb48513d19076569853be3e52263922..1afc146e80ab034ac4894875d5d5d4f50b43d04a 100644 --- a/vipra-ui/app/less/app.less +++ b/vipra-ui/app/less/app.less @@ -289,7 +289,7 @@ revolve-select, [revolve-select] { } .explorer { - @sidebar-width: 250px; + @sidebar-width: 300px; .sidebar { position: absolute; @@ -306,7 +306,7 @@ revolve-select, [revolve-select] { } } - .chart { + .center { position: absolute; top: 0; left: @sidebar-width; @@ -314,6 +314,10 @@ revolve-select, [revolve-select] { bottom: 0; } + .chart { + padding: 5px; + } + .topbar { background: #f9f9f9; width: 100%; @@ -326,7 +330,7 @@ revolve-select, [revolve-select] { } .topic-choice { - padding-top: 5px; + position: absolute; label { padding-right: 15px; 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 49a56366bbb062c79934803248bee976e4d3bc33..9f0a17a5bd5cc1e2b1dffa7aab9e034d5ee03af5 100644 --- a/vipra-util/src/main/java/de/vipra/util/Constants.java +++ b/vipra-util/src/main/java/de/vipra/util/Constants.java @@ -78,6 +78,13 @@ public class Constants { */ public static final int K_TOPIC_WORDS = 50; + /** + * This value is a weight to the rising decay caulculation of topic + * relevances. The higher this value, the more focus is put on later + * sequences containing more recent documents. Default 0. + */ + public static final double RISING_DECAY_LAMBDA = 0.0; + /** * Minimum likeliness of words. Words with lower likeliness are ignored. * Default 0.01. 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 f206b6ade3644ef0d38abb1931931ca6a81bb3a5..2589c49b6c1e2ac003046b744e432610090bc115 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 @@ -35,6 +35,15 @@ public class TopicFull implements Model<ObjectId>, Serializable { @QueryIgnore(multi = true) private Double varRelevance; + @QueryIgnore(multi = true) + private Double risingRelevance; + + @QueryIgnore(multi = true) + private Double fallingRelevance; + + @QueryIgnore(multi = true) + private Double risingDecayRelevance; + private Date created; private Date modified; @@ -85,6 +94,30 @@ public class TopicFull implements Model<ObjectId>, Serializable { this.varRelevance = varRelevance; } + public Double getRisingRelevance() { + return risingRelevance; + } + + public void setRisingRelevance(Double risingRelevance) { + this.risingRelevance = risingRelevance; + } + + public Double getFallingRelevance() { + return fallingRelevance; + } + + public void setFallingRelevance(Double fallingRelevance) { + this.fallingRelevance = fallingRelevance; + } + + public Double getRisingDecayRelevance() { + return risingDecayRelevance; + } + + public void setRisingDecayRelevance(Double risingDecayRelevance) { + this.risingDecayRelevance = risingDecayRelevance; + } + public Date getCreated() { return created; }