diff --git a/app/controllers/main.controller.js b/app/controllers/main.controller.js index d8e97d3..60fd188 100644 --- a/app/controllers/main.controller.js +++ b/app/controllers/main.controller.js @@ -1,161 +1,166 @@ var pageviewChartModel = require('../models/pageviewchart.model'); module.exports = function($scope, pageViews, searchService, chartService, $http) { chart = chartService.createChart('myChart', new pageviewChartModel()); $scope.dateFrom = new Date(Date.parse("2016-01-01")); $scope.dateTo = new Date(Date.parse("2016-04-01")); chart.getModel().setDateRange($scope.dateFrom, $scope.dateTo); $scope.groups = []; $scope.search = { str: "", list: [] }; $scope.chart = { selected: 'line' }; $scope.$watch('chart.selected', function() { chart.setType($scope.chart.selected); }); $scope.dateToStr = function(date) { var day = date.getDate() + ''; var month = date.getMonth() + 1 + ''; var year = date.getFullYear() + ''; var month = month < 10 ? '0' + month: month; var day = day < 10 ? '0' + day: day; return year+month+day; }; $scope.addNewArticle = function (name) { pageViews.query({ project: $scope.chosen.lang.wiki + '.' + $scope.chosen.proj.namespace, article: name, from: $scope.dateToStr($scope.dateFrom), to: $scope.dateToStr($scope.dateTo), }).$promise.then(function(result) { result.article.name = name; $scope.groups.push({ articles:[result.article], name: name }); chart.getModel().addDataset(name, result.article.views); }); $scope.search = { str: "", list: [] }; }; $scope.chosen = {}; function reloadAll() { var g = angular.copy($scope.groups, g); $scope.groups = []; chart.getModel().clearDatasets(); angular.forEach(g, function (val, key) { $scope.addNewArticle(val.name); }); chart.getModel().setDateRange($scope.dateFrom, $scope.dateTo); } $http.get('projects.json').then(function (res) { $scope.projects = res.data.projects; $scope.chosen.proj = res.data.projects[0]; $scope.chosen.lang = res.data.projects[0].languages[0]; }); $scope.changeChosen = function(name, dropdown) { $scope.chosen[dropdown] = name; }; $scope.searchArticle = function (searchstr) { return searchService.query({ namespace: $scope.chosen.lang.wiki + '.' + $scope.chosen.proj.namespace, str: searchstr }).then(function(response) { return response.data.url; }); }; + $scope.exportgraph = function(mime) { + console.log(mime); + if(!mime) return; + chart.getModel().exportgraph(mime); + }; // DATEPICKER // TODO: Move everything related to bottom bar date pickers // to separate controller $scope.today = function() { $scope.dt = new Date(); }; $scope.today(); $scope.clear = function() { $scope.dt = null; }; $scope.inlineOptions = { customClass: getDayClass, minDate: new Date(), showWeeks: true }; $scope.dateOptions = { formatYear: 'yy', maxDate: new Date(), minDate: new Date(), startingDay: 1 }; $scope.toggleMin = function() { $scope.inlineOptions.minDate = $scope.inlineOptions.minDate ? null : new Date(); $scope.dateOptions.minDate = $scope.inlineOptions.minDate; }; $scope.toggleMin(); $scope.openFrom = function() { $scope.popupFrom.opened = true; }; $scope.openTo = function() { $scope.popupTo.opened = true; }; $scope.setDate = function (year, month, day) { $scope.dt = new Date(year, month, day); }; $scope.popupFrom = { opened: false }; $scope.popupTo = { opened: false }; $scope.$watch('dateFrom', reloadAll); $scope.$watch('dateTo', reloadAll); function getDayClass(data) { var date = data.date, mode = data.mode; if (mode === 'day') { var dayToCheck = new Date(date).setHours(0, 0, 0, 0); for (var i = 0; i < $scope.events.length; i++) { var currentDay = new Date($scope.events[i].date).setHours(0, 0, 0, 0); if (dayToCheck === currentDay) { return $scope.events[i].status; } } } return ''; } }; diff --git a/app/directives/highchart.directive.js b/app/directives/highchart.directive.js index 7ba7d66..591924b 100644 --- a/app/directives/highchart.directive.js +++ b/app/directives/highchart.directive.js @@ -1,181 +1,210 @@ module.exports = function(chartService) { return { restrict: 'E', scope: {}, // isolate scope link: function(scope, element, attrs) { // var seriesAdapterInterface = { // type: 'string' // passed to highcharts as chart type, // init: function(chart); // initialize the chart // add: function(chart, datasets, name); // add a data series to the chart // setXAxis: function(chart); // configure the x-axis // }; var lineSeriesAdapter = { type: 'line', init: function(chart) { chart.xAxis[0].setCategories(scope.chartModel.getXAxisValues()); }, add: function(chart, datasets, name) { chart.addSeries({ name: name, data: datasets[name] }); }, setXAxis: function(chart) { chart.xAxis[0].setCategories(scope.chartModel.getXAxisValues()); } }; var pieSeriesAdapter = { type: 'pie', init: function(chart) { chart.xAxis[0].setCategories([]); chart.addSeries({name: 'Total Views', data: []}); }, add: function(chart, datasets, name) { chart.series[0].addPoint({ name: name, y: datasets[name].reduce(function(a, b) { return a + b; }, 0) }); }, setXAxis: function(chart) { } }; var columnSeriesAdapter = { type: 'column', init: function(chart) { chart.xAxis[0].setCategories([]); chart.addSeries({name: 'Total Views'}); }, add: function(chart, datasets, name) { console.log(name); chart.series[0].addPoint({ name: name, y: datasets[name].reduce(function(a, b) { return a + b; }, 0) }); }, setXAxis: function(chart) { } }; scope.chart = chartService.getChart(attrs.chartName); scope.chartModel = chart.getModel(); if (attrs.chartType == 'pie') { scope.chart.setType('pie'); scope.chartSeriesAdapter = pieSeriesAdapter; } else if (attrs.chartType == 'column') { scope.chart.setType('column'); scope.chartSeriesAdapter = columnSeriesAdapter; } else { scope.chart.setType('line'); scope.chartSeriesAdapter = lineSeriesAdapter; } $(function () { var initchart = function() { $(element).highcharts({ chart: { type: scope.chartSeriesAdapter.type, animation: true }, credits: { enabled: false }, plotOptions: { pie: { dataLabels: { format: '{point.name}: {point.percentage:.1f} %' } } }, tooltip: { shared: true, crosshairs: { width: 1, color: 'rgba(0,0,0,0.2)' } }, title: { text: 'Page Views' }, xAxis: {}, // configured a bit later by chartSeriesAdapter.init yAxis: { title: { text: 'Views' } + }, + + exporting: { + buttons: { + contextButton: { + enabled: false + } + }, + fallbackToExportServer: false } // series: configured a bit later by chartSeriesAdapter.init/add }); var chart = $(element).highcharts(); var datasets = scope.chartModel.getDatasets(); var setnames = Object.keys(datasets); scope.chartSeriesAdapter.init(chart); for (var i in setnames) { scope.chartSeriesAdapter.add(chart, datasets, setnames[i]); } }; initchart(); scope.chartModel.addEventListener('datasetadded', function(name) { scope.chartSeriesAdapter.add($(element).highcharts(), scope.chartModel.getDatasets(), name); }); + + scope.chartModel.addEventListener('exportclicked', function(mime) { + var chart = $(element).highcharts(); + console.log(mime); + if(!mime) return; + if(mime === 'print') + { + chart.print(); + } + else if (mime === 'image/svg+xml' || mime === 'application/pdf') + { + chart.exportChartLocal({ + type: mime, + filename: 'chart', + //width: 1280, + sourceWidth: 1280, + sourceHeight: 720 + }); + } + }); scope.chartModel.addEventListener('daterangechanged', function() { console.log('date change'); scope.chartSeriesAdapter.setXAxis($(element).highcharts()); initchart(); }); scope.$watch('chart.getType()', function(type) { if (type == 'pie') { scope.chartSeriesAdapter = pieSeriesAdapter; } else if (type == 'column') { scope.chartSeriesAdapter = columnSeriesAdapter; } else { scope.chartSeriesAdapter = lineSeriesAdapter; } initchart(); }); }); } }; }; diff --git a/app/index.html b/app/index.html index a43a8c3..4a32df2 100644 --- a/app/index.html +++ b/app/index.html @@ -1,154 +1,156 @@ Wikistats + +

No Results Found

{{group.name}}
  • {{article.name}}
  • diff --git a/app/models/pageviewchart.model.js b/app/models/pageviewchart.model.js index ebe1a33..a9adea9 100644 --- a/app/models/pageviewchart.model.js +++ b/app/models/pageviewchart.model.js @@ -1,217 +1,229 @@ /** * Pageviews chart data model. * * @author Mikael Forsberg * @version 20160502T2001 */ module.exports = function() { /** * Date range starting date. */ this.dateRangeFrom = new Date(); /** * Date range ending date. */ this.dateRangeTo = new Date(); /** * Y-value data sets. */ this.datasets = {}; // "constructor" begins this.dateRangeFrom.setUTCHours(0); this.dateRangeFrom.setUTCMinutes(0); this.dateRangeFrom.setUTCSeconds(0); this.dateRangeFrom.setUTCMilliseconds(0); this.dateRangeTo.setUTCHours(0); this.dateRangeTo.setUTCMinutes(0); this.dateRangeTo.setUTCSeconds(0); this.dateRangeTo.setUTCMilliseconds(0); // default date range is one week ending at today's date this.dateRangeFrom.setDate(this.dateRangeFrom.getDate() - 7); // create event slots this.events = [ ['daterangechanged'], ['datasetadded'], ['datasetremoved'], - ['datasetscleared'] + ['datasetscleared'], + ['exportclicked'] ]; // "constructor" ends /** * Search for and potentially return a named event. * * @param String which Name of event * @access private * @return Array if event exists, null otherwise */ this.findEvent = function(which) { var event = null; for (var i in this.events) { if (this.events[i][0] == which) { event = this.events[i]; break; } } return event; }; /** * Fire a named event, calling all registered handlers. * * @param String which Name of event * @param Object data Data to pass to handlers * @access private * @return void */ this.dispatchEvent = function(which, data) { var event = this.findEvent(which); if (event) { for (k = 1; k < event.length; ++k) { event[k](data); } } }; /** * Register an event handler to a named event. * * @param String which Name of event * @param Function fn Event handler callback function * @return void */ this.addEventListener = function(which, fn) { var event = this.findEvent(which); if (event) { event.push(fn); } }; /** * Set the date range. The starting date must be an earlier date * than the ending date for the range to be considered valid. * * @param Date from Start of date range * @param Date to End of date range * @return Boolean True if given a valid range, False otherwise. */ this.setDateRange = function(from, to) { if (from.getTime() >= to.getTime()) { return false; } var signalEvent = (from.getTime() != this.dateRangeFrom.getTime() || to.getTime() != this.dateRangeTo.getTime()); this.dateRangeFrom = from; this.dateRangeTo = to; if (signalEvent) { this.dispatchEvent('daterangechanged', null); } return true; }; /** * Add a Y-value dataset. There must be one numeric value in yValues * for each distinct date contained in the current date range, and the * values must be in chronological order. * * @param String name Name of new dataset * @param Array yValues Array of numeric Y-values * @return void */ this.addDataset = function(name, yValues) { this.datasets[name] = yValues; this.dispatchEvent('datasetadded', name); }; /** * Remove a dataset. * * @param String name Name of dataset to remove * @return void */ this.removeDataset = function(name) { delete this.datasets[name]; this.dispatchEvent('datasetremoved', name); }; /** * Remove all datasets. * * @return void */ this.clearDatasets = function() { this.datasets = {}; this.dispatchEvent('datasetscleared', null); }; - + + /** + * Export the graph + * + * @param String mime-type of the export format + * @return void + */ + this.exportgraph = function(mime) + { + this.dispatchEvent('exportclicked', mime); + }; + /** * Retrieve a datasets. * * @return Object */ this.getDataset = function(name) { return this.datasets[name]; }; /** * Retrieve all datasets. * * @return Object */ this.getDatasets = function() { return this.datasets; }; /** * Retrieve the X-axis "values" or "tick labels", i.e. the * distinct dates contained in the current date range. * * @return Date[] */ this.getXAxisValues = function() { var values = []; var at = new Date(this.dateRangeFrom); var stop = this.dateRangeTo.getTime(); while (at.getTime() < stop) { values.push(new Date(at).toISOString().slice(0, 10)); at.setDate(at.getDate() + 1); } return values; }; }; diff --git a/gulpfile.js b/gulpfile.js index f725d1a..403c854 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,71 +1,73 @@ var gulp = require('gulp'), //sass = require('gulp-sass'), browserSync = require('browser-sync').create(), uglify = require('gulp-uglify'), sourcemaps = require('gulp-sourcemaps'), minifyCSS = require('gulp-clean-css'), jshint = require('gulp-jshint'), source = require('vinyl-source-stream'), streamify = require('gulp-streamify'), browserify = require('browserify'), concat = require('gulp-concat'), buffer = require('vinyl-buffer'); gulp.task('default', ['copy','copyvendor', 'css', 'js', 'lint']); gulp.task('browserSync', function() { browserSync.init({ server: { baseDir: 'dist' }, }) }) gulp.task('copy', function() { return gulp.src(['app/*.html', 'assets/projects.json']) .pipe(gulp.dest('dist')) .pipe(browserSync.stream()); }) gulp.task('copyvendor', function() { return gulp.src(['node_modules/angular/**/*', 'node_modules/angular-resource/**/*', 'node_modules/angular-animate/**/*', 'node_modules/bootstrap-css-only/css/**/*', 'node_modules/angular-ui-bootstrap/dist/**/*', + 'node_modules/highcharts-exporting/exporting.js', + 'node_modules/highcharts-offline-exporting/offline-exporting.js', 'node_modules/highcharts/highcharts.js', 'node_modules/jquery/dist/*']) .pipe(gulp.dest('dist/vendor')) }) gulp.task('lint', function() { return gulp.src('app/**/*.js') .pipe(jshint()) .pipe(jshint.reporter('default')) //.pipe(jshint.reporter('fail')) }); gulp.task('watch', ['browserSync', 'default'], function(){ gulp.watch('assets/scss/**/*.s*ss', ['css']); gulp.watch('app/**/*.js', ['lint', 'js']); gulp.watch('app/*.html', ['copy']); }) gulp.task('css', function() { return gulp.src('assets/scss/**/*.s*ss') //.pipe(sass()) .pipe(concat('styles.min.css')) .pipe(minifyCSS()) .pipe(gulp.dest('dist/assets/css')) .pipe(browserSync.stream()); }) gulp.task('js', function () { return browserify('./app/index.js').ignore('angular').bundle() .pipe(source('app.min.js')) .pipe(buffer()) .pipe(sourcemaps.init({loadMaps: true})) .pipe(streamify(uglify())) .pipe(sourcemaps.write('.')) .pipe(gulp.dest('dist/assets/js')) }) diff --git a/package.json b/package.json index 0b5e553..3e9f006 100644 --- a/package.json +++ b/package.json @@ -1,50 +1,53 @@ { "name": "wikistats", "version": "0.0.0", "description": "Page view statistics tool for wikimedia", "main": "gulpfile.js", "directories": { "doc": "doc", "test": "test" }, "dependencies": { - "highcharts": "^4.2.4", - "jquery": "^2.2.3", "angular": "^1.5.3", "angular-resource": "^1.5.3", "browserify": "^13.0.0", "gulp-streamify": "^1.0.2", + "highcharts": "^4.2.4", + "highcharts-exporting": "^0.1.2", + "highcharts-offline-exporting": "^0.1.2", + "jquery": "^2.2.3", "karma": "^0.13.22", "protractor": "^3.2.2", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0" }, "devDependencies": { "angular-animate": "^1.5.5", "angular-ui-bootstrap": "^1.3.2", "bootstrap": "^3.3.6", "bootstrap-css-only": "^3.3.6", "browser-sync": "^2.11.2", "gulp": "^3.9.1", "gulp-clean-css": "^2.0.4", "gulp-concat": "^2.6.0", "gulp-jshint": "^2.0.0", "gulp-ng-annotate": "^2.0.0", "gulp-sass": "^2.2.0", "gulp-sourcemaps": "^1.6.0", "gulp-uglify": "^1.5.3", + "highcharts-exporting": "^0.1.2", "jshint": "^2.9.1" }, "scripts": { "test": "node node_modules/karma/bin/karma start test/karma.conf.js" }, "keywords": [ "wikimedia", "wikistats", "stats", "pageviews", "statistics" ], "author": "Emil Gedda and the Wikistats group", "license": "BSD-3-Clause" }