Santhosh has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/236764

Change subject: Stats: Weekly trends
......................................................................

Stats: Weekly trends

While doing this, as per Pau's suggestion, I changed
the colors(again).
Translations: Blue, Draft: Gray, Deleted: Red

Bug: T105192
Change-Id: I8301bcd7e595e8144a43622a27503b8ed9494bbf
---
M extension.json
M i18n/en.json
M i18n/qqq.json
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
7 files changed, 406 insertions(+), 75 deletions(-)


  git pull 
ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/ContentTranslation 
refs/changes/64/236764/1

diff --git a/extension.json b/extension.json
index c6d051f..76fc319 100644
--- a/extension.json
+++ b/extension.json
@@ -849,6 +849,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"
                        ]
                },
@@ -857,7 +859,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 e355a1f..567159b 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -164,6 +164,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 44cb907..03d15c3 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -169,6 +169,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.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 ef86b07..620e10c 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,
@@ -53,7 +65,14 @@
                                'cx-trend-translations-to',
                                $.uls.data.getAutonym( mw.config.get( 
'wgContentLanguage' ) )
                        ).escaped() ),
-                       $( '<div>' ).addClass( 'cx-stats-cumulative__lang' 
).append( this.$languageCumulativeGraph )
+                       $( '<div>' ).addClass( 'cx-stats-cumulative__lang' 
).append( this.$languageCumulativeGraph ),
+                       $( '<h2>' ).text( mw.msg( 
'cx-trend-published-translations-title' ) ),
+                       $( '<div>' ).addClass( '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-trend__lang' ).append( 
this.$langTranslatonTrendBarChart )
                );
 
                $.when(
@@ -69,6 +88,8 @@
                        self.renderHighlights();
                        self.drawCumulativeGraph( 'count' );
                        self.drawLanguageCumulativeGraph( 'count' );
+                       self.drawTranslationTrend();
+                       self.drawLangTranslationTrend();
                } );
                this.getCXStats().then( function ( data ) {
                        if ( !data || !data.query ) {
@@ -463,11 +484,11 @@
                                },
                                {
                                        label:  mw.msg( 
'cx-stats-draft-translations-title' ),
-                                       strokeColor: 'rgba(255, 153, 0, 1)',
-                                       pointColor: 'rgba(255, 153, 0, 1)',
+                                       strokeColor: '#777',
+                                       pointColor: '#777',
                                        pointStrokeColor: '#fff',
                                        pointHighlightFill: '#fff',
-                                       pointHighlightStroke: 'rgba(255, 153, 
0, 1)',
+                                       pointHighlightStroke: '#777',
                                        data: $.map( this.totalDraftTrend, 
function ( data ) {
                                                return data[ type ];
                                        } )
@@ -496,23 +517,23 @@
                        } ),
                        datasets: [
                                {
-                                       label:  mw.msg( 
'cx-stats-draft-translations-title' ),
-                                       strokeColor: 'rgba(255, 153, 0, 1)',
-                                       pointColor: 'rgba(255, 153, 0, 1 )',
+                                       label: mw.msg( 
'cx-stats-draft-translations-title' ),
+                                       strokeColor: '#777',
+                                       pointColor: '#777',
                                        pointStrokeColor: '#fff',
                                        pointHighlightFill: '#fff',
-                                       pointHighlightStroke: 
'rgba(255,153,0,1)',
+                                       pointHighlightStroke: '#777',
                                        data: $.map( this.languageDraftTrend, 
function ( data ) {
                                                return data[ type ];
                                        } )
                                },
                                {
-                                       label:  mw.msg( 'cx-trend-deletions' ),
-                                       strokeColor: 'rgba(255, 0, 0, 1)',
-                                       pointColor: 'rgba(255, 0, 0, 1 )',
+                                       label: mw.msg( 'cx-trend-deletions' ),
+                                       strokeColor: '#FF0000',
+                                       pointColor: 'FF0000',
                                        pointStrokeColor: '#fff',
                                        pointHighlightFill: '#fff',
-                                       pointHighlightStroke: 'rgba(255, 0, 
0,1)',
+                                       pointHighlightStroke: 'FF0000',
                                        data: $.map( 
this.languageDeletionTrend, function ( data ) {
                                                return data[ type ];
                                        } )
@@ -522,11 +543,11 @@
                                                'cx-trend-translations-to',
                                                $.uls.data.getAutonym( 
mw.config.get( 'wgContentLanguage' ) )
                                        ).escaped(),
-                                       strokeColor: 'rgba(52, 123, 255, 1)',
-                                       pointColor: 'rgba(52, 123, 255, 1)',
+                                       strokeColor: '#347BFF',
+                                       pointColor: '#347BFF',
                                        pointStrokeColor: '#fff',
                                        pointHighlightFill: '#fff',
-                                       pointHighlightStroke: 'rgba(52, 123, 
255, 1)',
+                                       pointHighlightStroke: '#347BFF',
                                        data: $.map( 
this.languageTranslationTrend, function ( data ) {
                                                return data[ type ];
                                        } )
@@ -542,6 +563,113 @@
                } );
 
                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 data, cxTrendChart, ctx, 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 ) {
@@ -651,13 +779,15 @@
                        if ( languageData[ i ] && new Date( totalData[ i ].date 
) > new Date( languageData[ i ].date ) ) {
                                totalData.splice( i, 0, {
                                        date: languageData[ i ].date,
-                                       count: totalData[ i - 1 ] ? totalData[ 
i - 1 ].count : 0
+                                       count: totalData[ i - 1 ] ? totalData[ 
i - 1 ].count : 0,
+                                       delta: totalData[ i - 1 ] ? totalData[ 
i - 1 ].delta : 0
                                } );
                        }
                        if ( !languageData[ i ] || new Date( totalData[ i 
].date ) < new Date( languageData[ i ].date ) ) {
                                languageData.splice( i, 0, {
                                        date: totalData[ i ].date,
-                                       count: languageData[ i - 1 ] ? 
languageData[ i - 1 ].count : 0
+                                       count: languageData[ i - 1 ] ? 
languageData[ i - 1 ].count : 0,
+                                       delta: languageData[ i - 1 ] ? 
languageData[ i - 1 ].delta : 0
                                } );
                        }
                }
diff --git a/modules/stats/styles/ext.cx.stats.less 
b/modules/stats/styles/ext.cx.stats.less
index 126b0fc..1ec6667 100644
--- a/modules/stats/styles/ext.cx.stats.less
+++ b/modules/stats/styles/ext.cx.stats.less
@@ -151,7 +151,9 @@
 }
 
 .cx-stats-cumulative__lang,
-.cx-stats-cumulative__total {
+.cx-stats-cumulative__total,
+.cx-stats-trend__lang,
+.cx-stats-trend__total {
        background-color: #fff;
 }
 

-- 
To view, visit https://gerrit.wikimedia.org/r/236764
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I8301bcd7e595e8144a43622a27503b8ed9494bbf
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/ContentTranslation
Gerrit-Branch: master
Gerrit-Owner: Santhosh <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to