diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/SequenceResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/SequenceResource.java index 4960daef6c84e8b73cbbdb14a3bc8bfab9202ad9..9fa08d25de4dc25155a297097ed71144aea763c3 100644 --- a/vipra-backend/src/main/java/de/vipra/rest/resource/SequenceResource.java +++ b/vipra-backend/src/main/java/de/vipra/rest/resource/SequenceResource.java @@ -65,8 +65,8 @@ public class SequenceResource { @GET @Produces(MediaType.APPLICATION_JSON) @Path("{id}") - public Response getSequence(@PathParam("id") final String id, @QueryParam("fields") final String fields) - throws ConfigException, IOException { + public Response getSequence(@PathParam("id") final String id, @QueryParam("fields") final String fields, + @QueryParam("topWords") final Integer topWords) throws ConfigException, IOException { final ResponseWrapper<SequenceFull> res = new ResponseWrapper<>(); if (id == null || id.trim().length() == 0) { res.addError(new APIError(Response.Status.BAD_REQUEST, "ID is empty", @@ -84,6 +84,8 @@ public class SequenceResource { } if (sequence != null) { + if (topWords != null && topWords >= 0) + sequence.getWords().subList(topWords, sequence.getWords().size()).clear(); return res.ok(sequence); } else { res.addError(new APIError(Response.Status.NOT_FOUND, "Resource not found", diff --git a/vipra-ui/app/html/directives/sequence-dropdown.html b/vipra-ui/app/html/directives/sequence-dropdown.html new file mode 100644 index 0000000000000000000000000000000000000000..d04583bd40981275cdc5ba7819f3576b7c3fd66c --- /dev/null +++ b/vipra-ui/app/html/directives/sequence-dropdown.html @@ -0,0 +1,5 @@ +<ol class="nya-bs-select nya-bs-condensed" ng-model="ngModel"> + <li value="{{sequence.id}}" class="nya-bs-option" ng-repeat="sequence in sequences"> + <a ng-bind="sequence.label"></a> + </li> +</ol> diff --git a/vipra-ui/app/html/topics/show.html b/vipra-ui/app/html/topics/show.html index fbbc8d31049640dee8cfd11dd73b1b170c6341f5..75e9aecfdcfac65004a46e4234ccb34094405c4f 100644 --- a/vipra-ui/app/html/topics/show.html +++ b/vipra-ui/app/html/topics/show.html @@ -55,22 +55,48 @@ </div> <div class="row"> <div class="col-md-12"> - <h3>Relevance over time</h3> - <div class="well well-sm"> - <div class="radio radio-inline"> - <input type="radio" id="seqAbsolute" ng-model="opts.seqstyle" value="absolute"> - <label for="seqAbsolute">Absolute</label> - </div> - <div class="radio radio-inline"> - <input type="radio" id="seqRelative" ng-model="opts.seqstyle" value="relative"> - <label for="seqRelative">Relative</label> + <h3>Relevance</h3> + <div class="wrapper"> + <div class="topbar"> + <small>Values:</small> + <div class="btn-group"> + <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"> + <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> </div> + <div class="area-chart" id="topic-seq" highcharts="topicSeq"></div> </div> </div> </div> - <div class="row row-spaced"> + <div class="row"> <div class="col-md-12"> - <div class="area-chart" id="topic-seq" highcharts="topicSeq"></div> + <h3>Words</h3> + <div class="wrapper"> + <div class="topbar"> + <small>Sequence:</small> + <sequence-dropdown ng-model="sequenceId" sequences="topic.sequences"></sequence-dropdown> + </div> + <table class="table table-condensed table-bordered table-hover" ng-show="sequence"> + <thead> + <tr> + <th ng-model="opts.sortwords" sort-by="id">Word</th> + <th ng-model="opts.sortwords" sort-by="likeliness">Likeliness</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="word in sequence.words | orderBy:opts.sortwords"> + <td ng-bind="word.id"></td> + <td ng-bind="word.likeliness.toFixed(4)"></td> + </tr> + </tbody> + </table> + </div> </div> </div> </div> diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js index 2ec72ad8c847726e64f67744911a02c0cc746662..4b1af8b2a79e2488ab3a90062310c460708274c5 100644 --- a/vipra-ui/app/js/controllers.js +++ b/vipra-ui/app/js/controllers.js @@ -552,11 +552,13 @@ /** * Topic Show route */ - app.controller('TopicsShowController', ['$scope', '$stateParams', '$timeout', 'TopicFactory', - function($scope, $stateParams, $timeout, TopicFactory) { + app.controller('TopicsShowController', ['$scope', '$stateParams', '$timeout', 'TopicFactory', 'SequenceFactory', + function($scope, $stateParams, $timeout, TopicFactory, SequenceFactory) { $scope.opts = { - seqstyle: 'absolute' + seqstyle: 'absolute', + chartstyle: 'areaspline', + sortwords: '-likeliness' }; TopicFactory.get({ @@ -565,26 +567,27 @@ $scope.topic = data; $scope.topicCreated = Vipra.formatDateTime($scope.topic.created); $scope.topicModified = Vipra.formatDateTime($scope.topic.modified); - - $scope.$watch('opts.seqstyle', function(style) { - if (!style || !$scope.topic || !$scope.topic.sequences) return; - console.log('redraw relevance chart'); - 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]); - } - - // highcharts configuration - $scope.topicSeq = areaRelevanceChart([{ name: $scope.topic.name, data: relevances }]); - }); + $scope.redrawGraph(); }, function(err) { $scope.errors = err; }); + $scope.redrawGraph = function() { + if (!$scope.topic || !$scope.topic.sequences) return; + console.log('redraw relevance chart'); + 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]); + } + + // highcharts configuration + $scope.topicSeq = areaRelevanceChart([{ name: $scope.topic.name, data: relevances }], null, $scope.opts.chartstyle); + }; + $scope.startRename = function() { $scope.origName = $scope.topic.name; $scope.isRename = true; @@ -614,6 +617,22 @@ $event.preventDefault(); } }; + + $scope.$watch('opts.seqstyle', $scope.redrawGraph); + $scope.$watch('opts.chartstyle', $scope.redrawGraph); + + $scope.$watch('sequenceId', function(sequence) { + if (sequence) { + SequenceFactory.get({ + id: sequence, + topWords: 20 + }, function(data) { + $scope.sequence = data; + }, function(err) { + $scope.errors = err; + }); + } + }); } ]); diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js index 06abf12bfaea98d7f6b88bf68ef1a195ed9b389f..0bca36a518f5a86527873e1d717b4716ed0c254d 100644 --- a/vipra-ui/app/js/directives.js +++ b/vipra-ui/app/js/directives.js @@ -2,7 +2,7 @@ * Vipra Application * Directives ******************************************************************************/ -/* globals angular, console, $ */ +/* globals angular, console, Vipra */ (function() { "use strict"; @@ -146,4 +146,58 @@ } ]); + app.directive('sequenceDropdown', [function() { + return { + scope: { + ngModel: '=', + sequences: '=' + }, + link: function($scope) { + $scope.$watch('sequences', function(newValue) { + if (newValue) { + for (var i = 0, s; i < $scope.sequences.length; i++) { + s = $scope.sequences[i]; + s.label = Vipra.sequenceLabel(s.window.startDate, s.window.windowResolution); + } + } + }); + }, + templateUrl: '/html/directives/sequence-dropdown.html' + }; + }]); + + app.directive('sortBy', function() { + return { + restrict: 'A', + scope: { + ngModel: '=', + sortBy: '@' + }, + link: function($scope, $elem) { + $elem.click(function() { + $scope.$apply($scope.check); + }); + + $scope.showCaret = function() { + return $scope.ngModel === $scope.sortBy || $scope.ngModel === '-' + $scope.sortBy; + }; + + $scope.check = function() { + $scope.reverse = false; + if ($scope.ngModel === $scope.sortBy) { + $scope.ngModel = '-' + $scope.sortBy; + $scope.reverse = true; + } else { + $scope.ngModel = $scope.sortBy; + $scope.reverse = false; + } + }; + + $scope.check(); + }, + transclude: true, + template: '<span ng-transclude></span> <span class="caret" ng-class="{\'caret-up\':reverse}" ng-show="showCaret()"></span>' + }; + }); + })(); diff --git a/vipra-ui/app/js/helpers.js b/vipra-ui/app/js/helpers.js index 7be66f275add61a4d6d5f4eb1ea955f6cfd3cef9..5970ba060a2d562de2e932edad48b943a411025d 100644 --- a/vipra-ui/app/js/helpers.js +++ b/vipra-ui/app/js/helpers.js @@ -2,7 +2,7 @@ * Vipra Application * Helpers & Polyfills ******************************************************************************/ -/* globals Vipra */ +/* globals Vipra, moment */ (function() { "use strict"; @@ -36,6 +36,47 @@ return 'id' + Math.random().toString(36).substring(7); }; + Vipra.sequenceLabel = function(date, res) { + date = moment(date); + var parts = []; + if (res === 'QUARTER') { + parts.push('Q' + date.quarter()); + parts.push(' '); + parts.push(date.year()); + } else { + switch (res) { + case 'SECOND': + parts.push(Vipra.zeroPad(date.second())); + /* falls through */ + case 'MINUTE': + parts.push(':'); + parts.push(Vipra.zeroPad(date.minute())); + /* falls through */ + case 'HOUR': + parts.push(':'); + parts.push(Vipra.zeroPad(date.hour())); + /* falls through */ + case 'DAY': + parts.push(' '); + parts.push(Vipra.zeroPad(date.day() + 1)); + /* falls through */ + case 'MONTH': + parts.push('-'); + parts.push(Vipra.zeroPad(date.month() + 1)); + /* falls through */ + case 'YEAR': + parts.push('-'); + parts.push(date.year()); + } + } + var rtrim = /^[\s\uFEFF\xA0:-]+|[\s\uFEFF\xA0:-]+$/g; + return parts.reverse().join('').replace(rtrim, ''); + }; + + Vipra.zeroPad = function(n) { + return n < 10 ? '0' + n : n; + }; + /** * Polyfills */ diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less index b987219ad9a9b08f8695ca34eeef85883cfe02ee..ca7415feb1b834cb33b6cc1db5b42fa202913fe2 100644 --- a/vipra-ui/app/less/app.less +++ b/vipra-ui/app/less/app.less @@ -219,6 +219,14 @@ a:hover { } } +.topbar { + background: #f9f9f9; + width: 100%; + padding: 5px; + vertical-align: middle; + margin-bottom: 10px; +} + .explorer { @sidebar-width: 300px; @sidebar-padding: 5px; @@ -245,12 +253,6 @@ a:hover { .chart { padding: 5px; } - .topbar { - background: #f9f9f9; - width: 100%; - padding: 5px; - vertical-align: middle; - } .sequence { flex: 1 0 0; } @@ -337,6 +339,15 @@ input[type=checkbox], height: 100%; } +[sort-by] { + cursor: pointer; +} + +.caret.caret-up { + border-top-width: 0; + border-bottom: 4px solid #000000; +} + @-moz-keyframes spin { 100% { -moz-transform: rotateY(360deg); diff --git a/vipra-ui/gulpfile.js b/vipra-ui/gulpfile.js index 95593c216fe5901672b570c3fbbe2b4a2268de3e..07c59711b24582a8847e0e43100f8a46bacf8e91 100644 --- a/vipra-ui/gulpfile.js +++ b/vipra-ui/gulpfile.js @@ -41,21 +41,21 @@ var assets = { gulp.task('less', function() { gulp.src('app/less/**/*.less') - .pipe(sourcemaps.init()) + //.pipe(sourcemaps.init()) .pipe(concat('app.css')) .pipe(less()) .pipe(cleancss()) - .pipe(sourcemaps.write('.')) + //.pipe(sourcemaps.write('.')) .pipe(gulp.dest('public/css')); }); gulp.task('js', function() { gulp.src('app/js/**/*.js') - .pipe(sourcemaps.init()) + //.pipe(sourcemaps.init()) .pipe(concat('app.js')) .pipe(ngannotate()) //.pipe(uglify()) - .pipe(sourcemaps.write('.')) + //.pipe(sourcemaps.write('.')) .pipe(gulp.dest('public/js')); });