jenkins-bot has submitted this change and it was merged.
Change subject: Stats: Weekly trends
......................................................................
Stats: Weekly trends
While doing this, as per Pau's suggestion, I changed
the colors (again).
Also upgraded Chart.js and added Chart.Bar.js for
bar charts.
Bug: T105192
Change-Id: I8301bcd7e595e8144a43622a27503b8ed9494bbf
---
M extension.json
M i18n/en.json
M i18n/qqq.json
A lib/chart.js/Chart.Bar.js
M lib/chart.js/Chart.Core.js
M lib/chart.js/Chart.Line.js
M modules/stats/ext.cx.stats.js
M modules/stats/styles/ext.cx.stats.less
8 files changed, 712 insertions(+), 63 deletions(-)
Approvals:
Nikerabbit: Looks good to me, approved
jenkins-bot: Verified
diff --git a/extension.json b/extension.json
index 5ed4bb8..383b0d0 100644
--- a/extension.json
+++ b/extension.json
@@ -867,6 +867,8 @@
"cx-stats-local-published-number",
"cx-stats-local-published",
"cx-stats-grouping-title",
+ "cx-trend-published-translations-title",
+ "cx-trend-translations-to-title",
"percent"
]
},
@@ -875,7 +877,8 @@
"remoteExtPath": "ContentTranslation/lib",
"scripts": [
"chart.js/Chart.Core.js",
- "chart.js/Chart.Line.js"
+ "chart.js/Chart.Line.js",
+ "chart.js/Chart.Bar.js"
]
},
"ext.cx.beta.notification": {
diff --git a/i18n/en.json b/i18n/en.json
index bf13237..159737a 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -169,6 +169,8 @@
"cx-stats-local-published-number": "$1 in $2",
"cx-stats-local-published": "$1 ($3) in $2",
"cx-stats-grouping-title": "{{PLURAL:$1|$1 translation|$1
translations}}",
+ "cx-trend-published-translations-title": "Translation trend",
+ "cx-trend-translations-to-title": "Translation trend to $1",
"cx-tools-missing-link-text": "{{GENDER:|Mark}} the page as missing to
encourage its creation.",
"cx-tools-missing-link-tooltip": "Translate (in new window)",
"cx-tools-missing-link-title": "Missing link",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 45c4b4b..7f2cae3 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -174,6 +174,8 @@
"cx-stats-local-published-number": "A message shown in
[[Special:CXStats]] as highlights of CX analytics. $1 is a number, $2 is
language name (autonym).",
"cx-stats-local-published": "A message shown in [[Special:CXStats]] as
highlights of CX analytics. $1 is a number, $2 is language name (autonym), $3
is percentage.",
"cx-stats-grouping-title": "Title text for language grouping based on
number of translations. $1 is number of
translations\n{{Identical|Translation}}",
+ "cx-trend-published-translations-title": "A message shown in
[[Special:CXStats]]",
+ "cx-trend-translations-to-title": "Label shown in the legend section
Content translation trends graph visualization.\n* $1 - language name",
"cx-tools-missing-link-text": "Message with instructions for marking
links as missing",
"cx-tools-missing-link-tooltip": "Tooltip that shows when hovering over
target link card link when working with missing links.\nClicking on link opens
a new translation view for the missing link.",
"cx-tools-missing-link-title": "Title for target link card when card is
used for working with missing links",
diff --git a/lib/chart.js/Chart.Bar.js b/lib/chart.js/Chart.Bar.js
new file mode 100644
index 0000000..81532b4
--- /dev/null
+++ b/lib/chart.js/Chart.Bar.js
@@ -0,0 +1,303 @@
+(function(){
+ "use strict";
+
+ var root = this,
+ Chart = root.Chart,
+ helpers = Chart.helpers;
+
+
+ var defaultConfig = {
+ //Boolean - Whether the scale should start at zero, or an order
of magnitude down from the lowest value
+ scaleBeginAtZero : true,
+
+ //Boolean - Whether grid lines are shown across the chart
+ scaleShowGridLines : true,
+
+ //String - Colour of the grid lines
+ scaleGridLineColor : "rgba(0,0,0,.05)",
+
+ //Number - Width of the grid lines
+ scaleGridLineWidth : 1,
+
+ //Boolean - Whether to show horizontal lines (except X axis)
+ scaleShowHorizontalLines: true,
+
+ //Boolean - Whether to show vertical lines (except Y axis)
+ scaleShowVerticalLines: true,
+
+ //Boolean - If there is a stroke on each bar
+ barShowStroke : true,
+
+ //Number - Pixel width of the bar stroke
+ barStrokeWidth : 2,
+
+ //Number - Spacing between each of the X value sets
+ barValueSpacing : 5,
+
+ //Number - Spacing between data sets within X values
+ barDatasetSpacing : 1,
+
+ //String - A legend template
+ legendTemplate : "<ul
class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length;
i++){%><li><span
style=\"background-color:<%=datasets[i].fillColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>"
+
+ };
+
+
+ Chart.Type.extend({
+ name: "Bar",
+ defaults : defaultConfig,
+ initialize: function(data){
+
+ //Expose options as a scope variable here so we can
access it in the ScaleClass
+ var options = this.options;
+
+ this.ScaleClass = Chart.Scale.extend({
+ offsetGridLines : true,
+ calculateBarX : function(datasetCount,
datasetIndex, barIndex){
+ //Reusable method for calculating the
xPosition of a given bar based on datasetIndex & width of the bar
+ var xWidth = this.calculateBaseWidth(),
+ xAbsolute =
this.calculateX(barIndex) - (xWidth/2),
+ barWidth =
this.calculateBarWidth(datasetCount);
+
+ return xAbsolute + (barWidth *
datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2;
+ },
+ calculateBaseWidth : function(){
+ return (this.calculateX(1) -
this.calculateX(0)) - (2*options.barValueSpacing);
+ },
+ calculateBarWidth : function(datasetCount){
+ //The padding between datasets is to
the right of each bar, providing that there are more than 1 dataset
+ var baseWidth =
this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing);
+
+ return (baseWidth / datasetCount);
+ }
+ });
+
+ this.datasets = [];
+
+ //Set up tooltip events on the chart
+ if (this.options.showTooltips){
+ helpers.bindEvents(this,
this.options.tooltipEvents, function(evt){
+ var activeBars = (evt.type !==
'mouseout') ? this.getBarsAtEvent(evt) : [];
+
+ this.eachBars(function(bar){
+ bar.restore(['fillColor',
'strokeColor']);
+ });
+ helpers.each(activeBars,
function(activeBar){
+ activeBar.fillColor =
activeBar.highlightFill;
+ activeBar.strokeColor =
activeBar.highlightStroke;
+ });
+ this.showTooltip(activeBars);
+ });
+ }
+
+ //Declare the extension of the default point, to cater
for the options passed in to the constructor
+ this.BarClass = Chart.Rectangle.extend({
+ strokeWidth : this.options.barStrokeWidth,
+ showStroke : this.options.barShowStroke,
+ ctx : this.chart.ctx
+ });
+
+ //Iterate through each of the datasets, and build this
into a property of the chart
+
helpers.each(data.datasets,function(dataset,datasetIndex){
+
+ var datasetObject = {
+ label : dataset.label || null,
+ fillColor : dataset.fillColor,
+ strokeColor : dataset.strokeColor,
+ bars : []
+ };
+
+ this.datasets.push(datasetObject);
+
+
helpers.each(dataset.data,function(dataPoint,index){
+ //Add a new point for each piece of
data, passing any required data to draw.
+ datasetObject.bars.push(new
this.BarClass({
+ value : dataPoint,
+ label : data.labels[index],
+ datasetLabel: dataset.label,
+ strokeColor :
dataset.strokeColor,
+ fillColor : dataset.fillColor,
+ highlightFill :
dataset.highlightFill || dataset.fillColor,
+ highlightStroke :
dataset.highlightStroke || dataset.strokeColor
+ }));
+ },this);
+
+ },this);
+
+ this.buildScale(data.labels);
+
+ this.BarClass.prototype.base = this.scale.endPoint;
+
+ this.eachBars(function(bar, index, datasetIndex){
+ helpers.extend(bar, {
+ width :
this.scale.calculateBarWidth(this.datasets.length),
+ x:
this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
+ y: this.scale.endPoint
+ });
+ bar.save();
+ }, this);
+
+ this.render();
+ },
+ update : function(){
+ this.scale.update();
+ // Reset any highlight colours before updating.
+ helpers.each(this.activeElements,
function(activeElement){
+ activeElement.restore(['fillColor',
'strokeColor']);
+ });
+
+ this.eachBars(function(bar){
+ bar.save();
+ });
+ this.render();
+ },
+ eachBars : function(callback){
+ helpers.each(this.datasets,function(dataset,
datasetIndex){
+ helpers.each(dataset.bars, callback, this,
datasetIndex);
+ },this);
+ },
+ getBarsAtEvent : function(e){
+ var barsArray = [],
+ eventPosition = helpers.getRelativePosition(e),
+ datasetIterator = function(dataset){
+ barsArray.push(dataset.bars[barIndex]);
+ },
+ barIndex;
+
+ for (var datasetIndex = 0; datasetIndex <
this.datasets.length; datasetIndex++) {
+ for (barIndex = 0; barIndex <
this.datasets[datasetIndex].bars.length; barIndex++) {
+ if
(this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){
+ helpers.each(this.datasets,
datasetIterator);
+ return barsArray;
+ }
+ }
+ }
+
+ return barsArray;
+ },
+ buildScale : function(labels){
+ var self = this;
+
+ var dataTotal = function(){
+ var values = [];
+ self.eachBars(function(bar){
+ values.push(bar.value);
+ });
+ return values;
+ };
+
+ var scaleOptions = {
+ templateString : this.options.scaleLabel,
+ height : this.chart.height,
+ width : this.chart.width,
+ ctx : this.chart.ctx,
+ textColor : this.options.scaleFontColor,
+ fontSize : this.options.scaleFontSize,
+ fontStyle : this.options.scaleFontStyle,
+ fontFamily : this.options.scaleFontFamily,
+ valuesCount : labels.length,
+ beginAtZero : this.options.scaleBeginAtZero,
+ integersOnly : this.options.scaleIntegersOnly,
+ calculateYRange: function(currentHeight){
+ var updatedRanges =
helpers.calculateScaleRange(
+ dataTotal(),
+ currentHeight,
+ this.fontSize,
+ this.beginAtZero,
+ this.integersOnly
+ );
+ helpers.extend(this, updatedRanges);
+ },
+ xLabels : labels,
+ font :
helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle,
this.options.scaleFontFamily),
+ lineWidth : this.options.scaleLineWidth,
+ lineColor : this.options.scaleLineColor,
+ showHorizontalLines :
this.options.scaleShowHorizontalLines,
+ showVerticalLines :
this.options.scaleShowVerticalLines,
+ gridLineWidth :
(this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
+ gridLineColor :
(this.options.scaleShowGridLines) ? this.options.scaleGridLineColor :
"rgba(0,0,0,0)",
+ padding : (this.options.showScale) ? 0 :
(this.options.barShowStroke) ? this.options.barStrokeWidth : 0,
+ showLabels : this.options.scaleShowLabels,
+ display : this.options.showScale
+ };
+
+ if (this.options.scaleOverride){
+ helpers.extend(scaleOptions, {
+ calculateYRange: helpers.noop,
+ steps: this.options.scaleSteps,
+ stepValue: this.options.scaleStepWidth,
+ min: this.options.scaleStartValue,
+ max: this.options.scaleStartValue +
(this.options.scaleSteps * this.options.scaleStepWidth)
+ });
+ }
+
+ this.scale = new this.ScaleClass(scaleOptions);
+ },
+ addData : function(valuesArray,label){
+ //Map the values array for each of the datasets
+ helpers.each(valuesArray,function(value,datasetIndex){
+ //Add a new point for each piece of data,
passing any required data to draw.
+ this.datasets[datasetIndex].bars.push(new
this.BarClass({
+ value : value,
+ label : label,
+ datasetLabel:
this.datasets[datasetIndex].label,
+ x:
this.scale.calculateBarX(this.datasets.length, datasetIndex,
this.scale.valuesCount+1),
+ y: this.scale.endPoint,
+ width :
this.scale.calculateBarWidth(this.datasets.length),
+ base : this.scale.endPoint,
+ strokeColor :
this.datasets[datasetIndex].strokeColor,
+ fillColor :
this.datasets[datasetIndex].fillColor
+ }));
+ },this);
+
+ this.scale.addXLabel(label);
+ //Then re-render the chart.
+ this.update();
+ },
+ removeData : function(){
+ this.scale.removeXLabel();
+ //Then re-render the chart.
+ helpers.each(this.datasets,function(dataset){
+ dataset.bars.shift();
+ },this);
+ this.update();
+ },
+ reflow : function(){
+ helpers.extend(this.BarClass.prototype,{
+ y: this.scale.endPoint,
+ base : this.scale.endPoint
+ });
+ var newScaleProps = helpers.extend({
+ height : this.chart.height,
+ width : this.chart.width
+ });
+ this.scale.update(newScaleProps);
+ },
+ draw : function(ease){
+ var easingDecimal = ease || 1;
+ this.clear();
+
+ var ctx = this.chart.ctx;
+
+ this.scale.draw(easingDecimal);
+
+ //Draw all the bars for each dataset
+
helpers.each(this.datasets,function(dataset,datasetIndex){
+ helpers.each(dataset.bars,function(bar,index){
+ if (bar.hasValue()){
+ bar.base = this.scale.endPoint;
+ //Transition then draw
+ bar.transition({
+ x :
this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
+ y :
this.scale.calculateY(bar.value),
+ width :
this.scale.calculateBarWidth(this.datasets.length)
+ }, easingDecimal).draw();
+ }
+ },this);
+
+ },this);
+ }
+ });
+
+
+}).call(this);
diff --git a/lib/chart.js/Chart.Core.js b/lib/chart.js/Chart.Core.js
old mode 100755
new mode 100644
index 5dccd2e..aa6c58d
--- a/lib/chart.js/Chart.Core.js
+++ b/lib/chart.js/Chart.Core.js
@@ -35,17 +35,17 @@
{
return
document.defaultView.getComputedStyle(element).getPropertyValue(dimension);
}
- }
+ };
- var width = this.width =
computeDimension(context.canvas,'Width');
- var height = this.height =
computeDimension(context.canvas,'Height');
+ var width = this.width =
computeDimension(context.canvas,'Width') || context.canvas.width;
+ var height = this.height =
computeDimension(context.canvas,'Height') || context.canvas.height;
// Firefox requires this to work correctly
context.canvas.width = width;
context.canvas.height = height;
- var width = this.width = context.canvas.width;
- var height = this.height = context.canvas.height;
+ width = this.width = context.canvas.width;
+ height = this.height = context.canvas.height;
this.aspectRatio = this.width / this.height;
//High pixel density displays - multiply the size of the canvas
height/width by the device pixel ratio, then scale.
helpers.retinaScale(this);
@@ -150,6 +150,9 @@
// String - Tooltip title font colour
tooltipTitleFontColor: "#fff",
+ // String - Tooltip title template
+ tooltipTitleTemplate: "<%= label%>",
+
// Number - pixel width of padding around tooltip text
tooltipYPadding: 6,
@@ -210,14 +213,18 @@
clone = helpers.clone = function(obj){
var objClone = {};
each(obj,function(value,key){
- if (obj.hasOwnProperty(key)) objClone[key] =
value;
+ if (obj.hasOwnProperty(key)){
+ objClone[key] = value;
+ }
});
return objClone;
},
extend = helpers.extend = function(base){
each(Array.prototype.slice.call(arguments,1),
function(extensionObject) {
each(extensionObject,function(value,key){
- if
(extensionObject.hasOwnProperty(key)) base[key] = value;
+ if
(extensionObject.hasOwnProperty(key)){
+ base[key] = value;
+ }
});
});
return base;
@@ -300,9 +307,9 @@
})(),
warn = helpers.warn = function(str){
//Method for warning of errors
- if (window.console && typeof window.console.warn ==
"function") console.warn(str);
+ if (window.console && typeof window.console.warn ===
"function") console.warn(str);
},
- amd = helpers.amd = (typeof define == 'function' && define.amd),
+ amd = helpers.amd = (typeof define === 'function' &&
define.amd),
//-- Math methods
isNumber = helpers.isNumber = function(n){
return !isNaN(parseFloat(n)) && isFinite(n);
@@ -328,7 +335,20 @@
},
getDecimalPlaces = helpers.getDecimalPlaces = function(num){
if (num%1!==0 && isNumber(num)){
- return num.toString().split(".")[1].length;
+ var s = num.toString();
+ if(s.indexOf("e-") < 0){
+ // no exponent, e.g. 0.01
+ return s.split(".")[1].length;
+ }
+ else if(s.indexOf(".") < 0) {
+ // no decimal point, e.g. 1e-9
+ return parseInt(s.split("e-")[1]);
+ }
+ else {
+ // exponent and decimal point, e.g.
1.23e-9
+ var parts = s.split(".")[1].split("e-");
+ return parts[0].length +
parseInt(parts[1]);
+ }
}
else {
return 0;
@@ -390,7 +410,7 @@
var maxValue = max(valuesArray),
minValue = min(valuesArray);
- // We need some degree of seperation here to calculate
the scales if all the values are the same
+ // We need some degree of separation here to calculate
the scales if all the values are the same
// Adding/minusing 0.5 will give us a range of 1.
if (maxValue === minValue){
maxValue += 0.5;
@@ -505,7 +525,7 @@
/* jshint ignore:end */
generateLabels = helpers.generateLabels =
function(templateString,numberOfSteps,graphMin,stepValue){
var labelsArray = new Array(numberOfSteps);
- if (labelTemplateString){
+ if (templateString){
each(labelsArray,function(val,index){
labelsArray[index] =
template(templateString,{value: (graphMin + (stepValue*(index+1)))});
});
@@ -526,7 +546,9 @@
return -1 * t * (t - 2);
},
easeInOutQuad: function (t) {
- if ((t /= 1 / 2) < 1) return 1 / 2 * t * t;
+ if ((t /= 1 / 2) < 1){
+ return 1 / 2 * t * t;
+ }
return -1 / 2 * ((--t) * (t - 2) - 1);
},
easeInCubic: function (t) {
@@ -536,7 +558,9 @@
return 1 * ((t = t / 1 - 1) * t * t + 1);
},
easeInOutCubic: function (t) {
- if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t;
+ if ((t /= 1 / 2) < 1){
+ return 1 / 2 * t * t * t;
+ }
return 1 / 2 * ((t -= 2) * t * t + 2);
},
easeInQuart: function (t) {
@@ -546,7 +570,9 @@
return -1 * ((t = t / 1 - 1) * t * t * t - 1);
},
easeInOutQuart: function (t) {
- if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t
* t;
+ if ((t /= 1 / 2) < 1){
+ return 1 / 2 * t * t * t * t;
+ }
return -1 / 2 * ((t -= 2) * t * t * t - 2);
},
easeInQuint: function (t) {
@@ -556,7 +582,9 @@
return 1 * ((t = t / 1 - 1) * t * t * t * t +
1);
},
easeInOutQuint: function (t) {
- if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t
* t * t;
+ if ((t /= 1 / 2) < 1){
+ return 1 / 2 * t * t * t * t * t;
+ }
return 1 / 2 * ((t -= 2) * t * t * t * t + 2);
},
easeInSine: function (t) {
@@ -575,60 +603,95 @@
return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 *
t / 1) + 1);
},
easeInOutExpo: function (t) {
- if (t === 0) return 0;
- if (t === 1) return 1;
- if ((t /= 1 / 2) < 1) return 1 / 2 *
Math.pow(2, 10 * (t - 1));
+ if (t === 0){
+ return 0;
+ }
+ if (t === 1){
+ return 1;
+ }
+ if ((t /= 1 / 2) < 1){
+ return 1 / 2 * Math.pow(2, 10 * (t -
1));
+ }
return 1 / 2 * (-Math.pow(2, -10 * --t) + 2);
},
easeInCirc: function (t) {
- if (t >= 1) return t;
+ if (t >= 1){
+ return t;
+ }
return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1);
},
easeOutCirc: function (t) {
return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t);
},
easeInOutCirc: function (t) {
- if ((t /= 1 / 2) < 1) return -1 / 2 *
(Math.sqrt(1 - t * t) - 1);
+ if ((t /= 1 / 2) < 1){
+ return -1 / 2 * (Math.sqrt(1 - t * t) -
1);
+ }
return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) +
1);
},
easeInElastic: function (t) {
var s = 1.70158;
var p = 0;
var a = 1;
- if (t === 0) return 0;
- if ((t /= 1) == 1) return 1;
- if (!p) p = 1 * 0.3;
+ if (t === 0){
+ return 0;
+ }
+ if ((t /= 1) == 1){
+ return 1;
+ }
+ if (!p){
+ p = 1 * 0.3;
+ }
if (a < Math.abs(1)) {
a = 1;
s = p / 4;
- } else s = p / (2 * Math.PI) * Math.asin(1 / a);
+ } else{
+ s = p / (2 * Math.PI) * Math.asin(1 /
a);
+ }
return -(a * Math.pow(2, 10 * (t -= 1)) *
Math.sin((t * 1 - s) * (2 * Math.PI) / p));
},
easeOutElastic: function (t) {
var s = 1.70158;
var p = 0;
var a = 1;
- if (t === 0) return 0;
- if ((t /= 1) == 1) return 1;
- if (!p) p = 1 * 0.3;
+ if (t === 0){
+ return 0;
+ }
+ if ((t /= 1) == 1){
+ return 1;
+ }
+ if (!p){
+ p = 1 * 0.3;
+ }
if (a < Math.abs(1)) {
a = 1;
s = p / 4;
- } else s = p / (2 * Math.PI) * Math.asin(1 / a);
+ } else{
+ s = p / (2 * Math.PI) * Math.asin(1 /
a);
+ }
return a * Math.pow(2, -10 * t) * Math.sin((t *
1 - s) * (2 * Math.PI) / p) + 1;
},
easeInOutElastic: function (t) {
var s = 1.70158;
var p = 0;
var a = 1;
- if (t === 0) return 0;
- if ((t /= 1 / 2) == 2) return 1;
- if (!p) p = 1 * (0.3 * 1.5);
+ if (t === 0){
+ return 0;
+ }
+ if ((t /= 1 / 2) == 2){
+ return 1;
+ }
+ if (!p){
+ p = 1 * (0.3 * 1.5);
+ }
if (a < Math.abs(1)) {
a = 1;
s = p / 4;
- } else s = p / (2 * Math.PI) * Math.asin(1 / a);
- if (t < 1) return -0.5 * (a * Math.pow(2, 10 *
(t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
+ } else {
+ s = p / (2 * Math.PI) * Math.asin(1 /
a);
+ }
+ if (t < 1){
+ return -0.5 * (a * Math.pow(2, 10 * (t
-= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));}
return a * Math.pow(2, -10 * (t -= 1)) *
Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1;
},
easeInBack: function (t) {
@@ -641,7 +704,9 @@
},
easeInOutBack: function (t) {
var s = 1.70158;
- if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t *
(((s *= (1.525)) + 1) * t - s));
+ if ((t /= 1 / 2) < 1){
+ return 1 / 2 * (t * t * (((s *=
(1.525)) + 1) * t - s));
+ }
return 1 / 2 * ((t -= 2) * t * (((s *= (1.525))
+ 1) * t + s) + 2);
},
easeInBounce: function (t) {
@@ -659,7 +724,9 @@
}
},
easeInOutBounce: function (t) {
- if (t < 1 / 2) return
easingEffects.easeInBounce(t * 2) * 0.5;
+ if (t < 1 / 2){
+ return easingEffects.easeInBounce(t *
2) * 0.5;
+ }
return easingEffects.easeOutBounce(t * 2 - 1) *
0.5 + 1 * 0.5;
}
},
@@ -762,14 +829,21 @@
});
},
getMaximumWidth = helpers.getMaximumWidth = function(domNode){
- var container = domNode.parentNode;
+ var container = domNode.parentNode,
+ padding = parseInt(getStyle(container,
'padding-left')) + parseInt(getStyle(container, 'padding-right'));
// TODO = check cross browser stuff with this.
- return container.clientWidth;
+ return container.clientWidth - padding;
},
getMaximumHeight = helpers.getMaximumHeight = function(domNode){
- var container = domNode.parentNode;
+ var container = domNode.parentNode,
+ padding = parseInt(getStyle(container,
'padding-bottom')) + parseInt(getStyle(container, 'padding-top'));
// TODO = check cross browser stuff with this.
- return container.clientHeight;
+ return container.clientHeight - padding;
+ },
+ getStyle = helpers.getStyle = function (el, property) {
+ return el.currentStyle ?
+ el.currentStyle[property] :
+ document.defaultView.getComputedStyle(el,
null).getPropertyValue(property);
},
getMaximumSize = helpers.getMaximumSize =
helpers.getMaximumWidth, // legacy support
retinaScale = helpers.retinaScale = function(chart){
@@ -844,7 +918,7 @@
},
stop : function(){
// Stops any current animation loop occuring
- cancelAnimFrame(this.animationFrame);
+ Chart.animationService.cancelAnimation(this);
return this;
},
resize : function(callback){
@@ -868,15 +942,26 @@
if (reflow){
this.reflow();
}
+
if (this.options.animation && !reflow){
- helpers.animationLoop(
- this.draw,
- this.options.animationSteps,
- this.options.animationEasing,
- this.options.onAnimationProgress,
- this.options.onAnimationComplete,
- this
- );
+ var animation = new Chart.Animation();
+ animation.numSteps =
this.options.animationSteps;
+ animation.easing = this.options.animationEasing;
+
+ // render function
+ animation.render = function(chartInstance,
animationObject) {
+ var easingFunction =
helpers.easingEffects[animationObject.easing];
+ var stepDecimal =
animationObject.currentStep / animationObject.numSteps;
+ var easeDecimal =
easingFunction(stepDecimal);
+
+ chartInstance.draw(easeDecimal,
stepDecimal, animationObject.currentStep);
+ };
+
+ // user events
+ animation.onAnimationProgress =
this.options.onAnimationProgress;
+ animation.onAnimationComplete =
this.options.onAnimationComplete;
+
+ Chart.animationService.addAnimation(this,
animation);
}
else{
this.draw();
@@ -1015,7 +1100,7 @@
labels: tooltipLabels,
legendColors: tooltipColors,
legendColorBackground :
this.options.multiTooltipKeyBackground,
- title: ChartElements[0].label,
+ title:
template(this.options.tooltipTitleTemplate,ChartElements[0]),
chart: this.chart,
ctx: this.chart.ctx,
custom:
this.options.customTooltips
@@ -1283,6 +1368,16 @@
}
});
+ Chart.Animation = Chart.Element.extend({
+ currentStep: null, // the current animation step
+ numSteps: 60, // default number of steps
+ easing: "", // the easing to use for this animation
+ render: null, // render function used by the animation service
+
+ onAnimationProgress: null, // user specified callback to fire
on each step of the animation
+ onAnimationComplete: null, // user specified callback to fire
when the animation finishes
+ });
+
Chart.Tooltip = Chart.Element.extend({
draw : function(){
@@ -1466,7 +1561,7 @@
for (var i=0; i<=this.steps; i++){
this.yLabels.push(template(this.templateString,{value:(this.min + (i *
this.stepValue)).toFixed(stepDecimalPlaces)}));
}
- this.yLabelWidth = (this.display && this.showLabels) ?
longestText(this.ctx,this.font,this.yLabels) : 0;
+ this.yLabelWidth = (this.display && this.showLabels) ?
longestText(this.ctx,this.font,this.yLabels) + 10 : 0;
},
addXLabel : function(label){
this.xLabels.push(label);
@@ -1489,6 +1584,9 @@
// Apply padding settings to the start and end point.
this.startPoint += this.padding;
this.endPoint -= this.padding;
+
+ // Cache the starting endpoint, excluding the space for
x labels
+ var cachedEndPoint = this.endPoint;
// Cache the starting height, so can determine if we
need to recalculate the scale yAxis
var cachedHeight = this.endPoint - this.startPoint,
@@ -1521,6 +1619,7 @@
// Only go through the xLabel loop again if the
yLabel width has changed
if (cachedYLabelWidth < this.yLabelWidth){
+ this.endPoint = cachedEndPoint;
this.calculateXLabelRotation();
}
}
@@ -1539,7 +1638,7 @@
this.xScalePaddingRight = lastWidth/2 + 3;
- this.xScalePaddingLeft = (firstWidth/2 >
this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10;
+ this.xScalePaddingLeft = (firstWidth/2 >
this.yLabelWidth) ? firstWidth/2 : this.yLabelWidth;
this.xLabelRotation = 0;
if (this.display){
@@ -1558,7 +1657,7 @@
lastRotated = cosRotation * lastWidth;
// We're right aligning the text now.
- if (firstRotated + this.fontSize / 2 >
this.yLabelWidth + 8){
+ if (firstRotated + this.fontSize / 2 >
this.yLabelWidth){
this.xScalePaddingLeft =
firstRotated + this.fontSize / 2;
}
this.xScalePaddingRight =
this.fontSize/2;
@@ -1984,6 +2083,90 @@
}
});
+ Chart.animationService = {
+ frameDuration: 17,
+ animations: [],
+ dropFrames: 0,
+ addAnimation: function(chartInstance, animationObject) {
+ for (var index = 0; index < this.animations.length; ++
index){
+ if (this.animations[index].chartInstance ===
chartInstance){
+ // replacing an in progress animation
+ this.animations[index].animationObject
= animationObject;
+ return;
+ }
+ }
+
+ this.animations.push({
+ chartInstance: chartInstance,
+ animationObject: animationObject
+ });
+
+ // If there are no animations queued, manually
kickstart a digest, for lack of a better word
+ if (this.animations.length == 1) {
+ helpers.requestAnimFrame.call(window,
this.digestWrapper);
+ }
+ },
+ // Cancel the animation for a given chart instance
+ cancelAnimation: function(chartInstance) {
+ var index = helpers.findNextWhere(this.animations,
function(animationWrapper) {
+ return animationWrapper.chartInstance ===
chartInstance;
+ });
+
+ if (index)
+ {
+ this.animations.splice(index, 1);
+ }
+ },
+ // calls startDigest with the proper context
+ digestWrapper: function() {
+
Chart.animationService.startDigest.call(Chart.animationService);
+ },
+ startDigest: function() {
+
+ var startTime = Date.now();
+ var framesToDrop = 0;
+
+ if(this.dropFrames > 1){
+ framesToDrop = Math.floor(this.dropFrames);
+ this.dropFrames -= framesToDrop;
+ }
+
+ for (var i = 0; i < this.animations.length; i++) {
+
+ if
(this.animations[i].animationObject.currentStep === null){
+
this.animations[i].animationObject.currentStep = 0;
+ }
+
+ this.animations[i].animationObject.currentStep
+= 1 + framesToDrop;
+
if(this.animations[i].animationObject.currentStep >
this.animations[i].animationObject.numSteps){
+
this.animations[i].animationObject.currentStep =
this.animations[i].animationObject.numSteps;
+ }
+
+
this.animations[i].animationObject.render(this.animations[i].chartInstance,
this.animations[i].animationObject);
+
+ if
(this.animations[i].animationObject.currentStep ==
this.animations[i].animationObject.numSteps){
+ // executed the last frame. Remove the
animation.
+ this.animations.splice(i, 1);
+ // Keep the index in place to offset
the splice
+ i--;
+ }
+ }
+
+ var endTime = Date.now();
+ var delay = endTime - startTime - this.frameDuration;
+ var frameDelay = delay / this.frameDuration;
+
+ if(frameDelay > 1){
+ this.dropFrames += frameDelay;
+ }
+
+ // Do we have more stuff to animate?
+ if (this.animations.length > 0){
+ helpers.requestAnimFrame.call(window,
this.digestWrapper);
+ }
+ }
+ };
+
// Attach global event to resize each chart instance when the browser
resizes
helpers.addEvent(window, "resize", (function(){
// Basic debounce of resize function so it doesn't hurt
performance when resizing browser.
diff --git a/lib/chart.js/Chart.Line.js b/lib/chart.js/Chart.Line.js
index 34ad85b..fdf1b01 100644
--- a/lib/chart.js/Chart.Line.js
+++ b/lib/chart.js/Chart.Line.js
@@ -50,7 +50,10 @@
datasetFill : true,
//String - A legend template
- legendTemplate : "<ul
class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length;
i++){%><li><span
style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
+ legendTemplate : "<ul
class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length;
i++){%><li><span
style=\"background-color:<%=datasets[i].strokeColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>",
+
+ //Boolean - Whether to horizontally center the label and point
dot inside the grid
+ offsetGridLines : false
};
@@ -61,6 +64,7 @@
initialize: function(data){
//Declare the extension of the default point, to cater
for the options passed in to the constructor
this.PointClass = Chart.Point.extend({
+ offsetGridLines : this.options.offsetGridLines,
strokeWidth : this.options.pointDotStrokeWidth,
radius : this.options.pointDotRadius,
display: this.options.pointDot,
@@ -176,6 +180,7 @@
width : this.chart.width,
ctx : this.chart.ctx,
textColor : this.options.scaleFontColor,
+ offsetGridLines : this.options.offsetGridLines,
fontSize : this.options.scaleFontSize,
fontStyle : this.options.scaleFontStyle,
fontFamily : this.options.scaleFontFamily,
@@ -226,6 +231,7 @@
this.datasets[datasetIndex].points.push(new
this.PointClass({
value : value,
label : label,
+ datasetLabel:
this.datasets[datasetIndex].label,
x:
this.scale.calculateX(this.scale.valuesCount+1),
y: this.scale.endPoint,
strokeColor :
this.datasets[datasetIndex].pointStrokeColor,
@@ -269,6 +275,7 @@
return helpers.findPreviousWhere(collection,
hasValue, index) || point;
};
+ if (!this.scale) return;
this.scale.draw(easingDecimal);
@@ -288,7 +295,7 @@
},this);
- // Control points need to be calculated in a
seperate loop, because we need to know the current x/y of the point
+ // Control points need to be calculated in a
separate loop, because we need to know the current x/y of the point
// This would cause issues when there is no
animation, because the y of the next point would be 0, so beziers would be
skewed
if (this.options.bezierCurve){
helpers.each(pointsWithValues,
function(point, index){
@@ -349,7 +356,9 @@
}
}, this);
- ctx.stroke();
+ if (this.options.datasetStroke) {
+ ctx.stroke();
+ }
if (this.options.datasetFill &&
pointsWithValues.length > 0){
//Round off the line by going to the
base of the chart, back to the start, then fill.
diff --git a/modules/stats/ext.cx.stats.js b/modules/stats/ext.cx.stats.js
index 17ce811..7c72029 100644
--- a/modules/stats/ext.cx.stats.js
+++ b/modules/stats/ext.cx.stats.js
@@ -44,6 +44,18 @@
height: 400
} );
+ this.$translatonTrendBarChart = $( '<canvas>' ).attr( {
+ id: 'cxtrendchart',
+ width: this.$container.width() - 200, // Leave a 200px
margin buffer to avoid overflow
+ height: 400
+ } );
+
+ this.$langTranslatonTrendBarChart = $( '<canvas>' ).attr( {
+ id: 'cxlangtrendchart',
+ width: this.$container.width() - 200, // Leave a 200px
margin buffer to avoid overflow
+ height: 400
+ } );
+
this.$container.append(
$spinner,
this.$highlights,
@@ -57,7 +69,18 @@
).escaped() ),
$( '<div>' )
.addClass( 'cx-stats-graph
cx-stats-cumulative-lang' )
- .append( this.$languageCumulativeGraph )
+ .append( this.$languageCumulativeGraph ),
+ $( '<h2>' ).text( mw.msg(
'cx-trend-published-translations-title' ) ),
+ $( '<div>' )
+ .addClass( 'cx-stats-graph
cx-stats-trend-total' )
+ .append( this.$translatonTrendBarChart ),
+ $( '<h2>' ).text( mw.message(
+ 'cx-trend-translations-to-title',
+ $.uls.data.getAutonym( mw.config.get(
'wgContentLanguage' ) )
+ ).escaped() ),
+ $( '<div>' )
+ .addClass( 'cx-stats-graph cx-stats-trend-lang'
)
+ .append( this.$langTranslatonTrendBarChart )
);
$.when(
@@ -73,6 +96,8 @@
self.renderHighlights();
self.drawCumulativeGraph( 'count' );
self.drawLanguageCumulativeGraph( 'count' );
+ self.drawTranslationTrend();
+ self.drawLangTranslationTrend();
} );
this.getCXStats().then( function ( data ) {
if ( !data || !data.query ) {
@@ -93,7 +118,7 @@
$parenthesizedTrend, $trendInLanguage,
fmt = mw.language.convertNumber; // Shortcut
- getTrend = function( data ) {
+ getTrend = function ( data ) {
var total, trend, thisWeek;
if ( data.length < 3 ) {
@@ -459,7 +484,6 @@
datasets: [
{
label: mw.msg(
'cx-trend-all-translations' ),
- fillColor: '#347BFF',
strokeColor: '#347BFF',
pointColor: '#347BFF',
pointStrokeColor: '#fff',
@@ -471,7 +495,6 @@
},
{
label: mw.msg(
'cx-stats-draft-translations-title' ),
- fillColor: '#777',
strokeColor: '#777',
pointColor: '#777',
pointStrokeColor: '#fff',
@@ -518,10 +541,10 @@
{
label: mw.msg( 'cx-trend-deletions' ),
strokeColor: '#FF0000',
- pointColor: '#FF0000',
+ pointColor: 'FF0000',
pointStrokeColor: '#fff',
pointHighlightFill: '#fff',
- pointHighlightStroke: '#FF0000',
+ pointHighlightStroke: 'FF0000',
data: $.map(
this.languageDeletionTrend, function ( data ) {
return data[ type ];
} )
@@ -553,6 +576,114 @@
this.$container.find( '.cx-stats-cumulative-lang' ).append(
cxCumulativeGraph.generateLegend() );
};
+ CXStats.prototype.drawTranslationTrend = function () {
+ var data, cxTrendChart, ctx, type = 'delta';
+
+ ctx = this.$translatonTrendBarChart[ 0 ].getContext( '2d' );
+ data = {
+ labels: $.map( this.totalTranslationTrend, function (
data ) {
+ return data.date;
+ } ),
+ datasets: [
+ {
+ label: mw.msg(
'cx-trend-all-translations' ),
+ strokeColor: '#347BFF',
+ fillColor: '#347BFF',
+ pointColor: '#347BFF',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: '#347BFF',
+ data: $.map(
this.totalTranslationTrend, function ( data ) {
+ return data[ type ];
+ } )
+ },
+ {
+ label: mw.msg(
'cx-stats-draft-translations-title' ),
+ strokeColor: '#777',
+ fillColor: '#777',
+ pointColor: '#777',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: '#777',
+ data: $.map( this.totalDraftTrend,
function ( data ) {
+ return data[ type ];
+ } )
+ }
+ ]
+ };
+
+ /*global Chart:false */
+ cxTrendChart = new Chart( ctx ).Bar( data, {
+ responsive: true,
+ barDatasetSpacing: 0,
+ legendTemplate: '<ul><% for (var i=0;
i<datasets.length; i++){%><li
style=\"color:<%=datasets[i].strokeColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'
+ } );
+
+ this.$container.find( '.cx-stats-trend-total' ).append(
cxTrendChart.generateLegend() );
+ };
+
+ CXStats.prototype.drawLangTranslationTrend = function () {
+ var ctx, data, cxTrendChart,
+ type = 'delta';
+
+ ctx = this.$langTranslatonTrendBarChart[ 0 ].getContext( '2d' );
+ data = {
+ labels: $.map( this.languageTranslationTrend, function
( data ) {
+ return data.date;
+ } ),
+ datasets: [
+ {
+ label: mw.message(
+ 'cx-trend-translations-to',
+ $.uls.data.getAutonym(
mw.config.get( 'wgContentLanguage' ) )
+ ).escaped(),
+ strokeColor: '#347BFF',
+ fillColor: '#347BFF',
+ pointColor: '#347BFF',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: '#347BFF',
+ data: $.map(
this.languageTranslationTrend, function ( data ) {
+ return data[ type ];
+ } )
+ },
+ {
+ label: mw.msg(
'cx-stats-draft-translations-title' ),
+ strokeColor: '#777',
+ fillColor: '#777',
+ pointColor: '#777',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: '#777',
+ data: $.map( this.languageDraftTrend,
function ( data ) {
+ return data[ type ];
+ } )
+ },
+ {
+ label: mw.msg( 'cx-trend-deletions' ),
+ strokeColor: '#FF0000',
+ fillColor: '#FF0000',
+ pointColor: '#FF0000',
+ pointStrokeColor: '#fff',
+ pointHighlightFill: '#fff',
+ pointHighlightStroke: '#FF0000',
+ data: $.map(
this.languageDeletionTrend, function ( data ) {
+ return data[ type ];
+ } )
+ }
+ ]
+ };
+
+ /*global Chart:false */
+ cxTrendChart = new Chart( ctx ).Bar( data, {
+ responsive: true,
+ barDatasetSpacing: 0,
+ legendTemplate: '<ul><% for (var i=0;
i<datasets.length; i++){%><li
style=\"color:<%=datasets[i].strokeColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'
+ } );
+
+ this.$container.find( '.cx-stats-trend-lang' ).append(
cxTrendChart.generateLegend() );
+ };
+
CXStats.prototype.transformJsonToModel = function ( records ) {
var i, record, language, status, count, translators,
sourceLanguage, targetLanguage,
diff --git a/modules/stats/styles/ext.cx.stats.less
b/modules/stats/styles/ext.cx.stats.less
index 0039f67..3c4ce11 100644
--- a/modules/stats/styles/ext.cx.stats.less
+++ b/modules/stats/styles/ext.cx.stats.less
@@ -160,6 +160,22 @@
.cx-stats-graph {
background-color: #fff;
+ border: 1px solid #999;
+
+ // Legend styling
+ li {
+ display: inline;
+ padding-right: 20px;
+
+ &:before {
+ content: '\25A0';
+ max-width: 0;
+ max-height: 0;
+ left: -10px;
+ top: 0;
+ font-size: 20px;
+ }
+ }
}
.cx-stats-highlights {
--
To view, visit https://gerrit.wikimedia.org/r/236764
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I8301bcd7e595e8144a43622a27503b8ed9494bbf
Gerrit-PatchSet: 7
Gerrit-Project: mediawiki/extensions/ContentTranslation
Gerrit-Branch: master
Gerrit-Owner: Santhosh <[email protected]>
Gerrit-Reviewer: Amire80 <[email protected]>
Gerrit-Reviewer: Nikerabbit <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits