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
+
+
{{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"
}