From 7d240eee9d6fb4a8bd5700d018c5d32747b6984b Mon Sep 17 00:00:00 2001 From: Nick Downie Date: Sun, 29 Jun 2014 17:34:56 +0100 Subject: [PATCH 1/9] Update ignored files --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 9bea4330f05..3969d0aade1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ .DS_Store + +node_modules/* +custom/* + +docs/index.md From be2f31a3518b99009782804dc9ed0ad8b9245fc4 Mon Sep 17 00:00:00 2001 From: Nick Downie Date: Sun, 29 Jun 2014 17:35:35 +0100 Subject: [PATCH 2/9] Change to bower.json --- component.json => bower.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename component.json => bower.json (100%) diff --git a/component.json b/bower.json similarity index 100% rename from component.json rename to bower.json From a04082dba11ed63adb0a96c093a414cbffd08010 Mon Sep 17 00:00:00 2001 From: Nick Downie Date: Sun, 29 Jun 2014 17:38:59 +0100 Subject: [PATCH 3/9] Remove site from repo --- docs/Chart.js | 1443 --------------------------------------- docs/index.html | 766 --------------------- docs/prettify.css | 38 -- docs/prettify.js | 28 - docs/prettify.less | 33 - docs/styles.css | 263 ------- docs/styles.less | 185 ----- samples/polarArea.html | 44 -- samples/sixup.html | 155 ----- site/assets/6charts.png | Bin 79923 -> 0 bytes site/assets/Chart.js | 1443 --------------------------------------- site/assets/effects.js | 320 --------- site/assets/excanvas.js | 1416 -------------------------------------- site/assets/html.png | Bin 83653 -> 0 bytes site/assets/simple.png | Bin 48416 -> 0 bytes site/index.html | 141 ---- site/styles.css | 205 ------ 17 files changed, 6480 deletions(-) delete mode 100755 docs/Chart.js delete mode 100644 docs/index.html delete mode 100644 docs/prettify.css delete mode 100644 docs/prettify.js delete mode 100644 docs/prettify.less delete mode 100644 docs/styles.css delete mode 100644 docs/styles.less delete mode 100755 samples/polarArea.html delete mode 100644 samples/sixup.html delete mode 100644 site/assets/6charts.png delete mode 100755 site/assets/Chart.js delete mode 100644 site/assets/effects.js delete mode 100644 site/assets/excanvas.js delete mode 100644 site/assets/html.png delete mode 100644 site/assets/simple.png delete mode 100644 site/index.html delete mode 100644 site/styles.css diff --git a/docs/Chart.js b/docs/Chart.js deleted file mode 100755 index a25acef1d6b..00000000000 --- a/docs/Chart.js +++ /dev/null @@ -1,1443 +0,0 @@ -/*! - * Chart.js - * http://chartjs.org/ - * - * Copyright 2013 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ - -//Define the global Chart Variable as a class. -var Chart = function(context){ - - var chart = this; - - - //Easing functions adapted from Robert Penner's easing equations - //http://www.robertpenner.com/easing/ - - var animationOptions = { - linear : function (t){ - return t; - }, - easeInQuad: function (t) { - return t*t; - }, - easeOutQuad: function (t) { - return -1 *t*(t-2); - }, - easeInOutQuad: function (t) { - if ((t/=1/2) < 1) return 1/2*t*t; - return -1/2 * ((--t)*(t-2) - 1); - }, - easeInCubic: function (t) { - return t*t*t; - }, - easeOutCubic: function (t) { - return 1*((t=t/1-1)*t*t + 1); - }, - easeInOutCubic: function (t) { - if ((t/=1/2) < 1) return 1/2*t*t*t; - return 1/2*((t-=2)*t*t + 2); - }, - easeInQuart: function (t) { - return t*t*t*t; - }, - easeOutQuart: function (t) { - 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; - return -1/2 * ((t-=2)*t*t*t - 2); - }, - easeInQuint: function (t) { - return 1*(t/=1)*t*t*t*t; - }, - easeOutQuint: function (t) { - 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; - return 1/2*((t-=2)*t*t*t*t + 2); - }, - easeInSine: function (t) { - return -1 * Math.cos(t/1 * (Math.PI/2)) + 1; - }, - easeOutSine: function (t) { - return 1 * Math.sin(t/1 * (Math.PI/2)); - }, - easeInOutSine: function (t) { - return -1/2 * (Math.cos(Math.PI*t/1) - 1); - }, - easeInExpo: function (t) { - return (t==0) ? 1 : 1 * Math.pow(2, 10 * (t/1 - 1)); - }, - easeOutExpo: function (t) { - 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)); - return 1/2 * (-Math.pow(2, -10 * --t) + 2); - }, - easeInCirc: function (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); - 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*.3; - if (a < Math.abs(1)) { a=1; var s=p/4; } - else var 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*.3; - if (a < Math.abs(1)) { a=1; var s=p/4; } - else var 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*(.3*1.5); - if (a < Math.abs(1)) { a=1; var s=p/4; } - else var s = p/(2*Math.PI) * Math.asin (1/a); - if (t < 1) return -.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 )*.5 + 1; - }, - easeInBack: function (t) { - var s = 1.70158; - return 1*(t/=1)*t*((s+1)*t - s); - }, - easeOutBack: function (t) { - var s = 1.70158; - return 1*((t=t/1-1)*t*((s+1)*t + s) + 1); - }, - easeInOutBack: function (t) { - var s = 1.70158; - 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) { - return 1 - animationOptions.easeOutBounce (1-t); - }, - easeOutBounce: function (t) { - if ((t/=1) < (1/2.75)) { - return 1*(7.5625*t*t); - } else if (t < (2/2.75)) { - return 1*(7.5625*(t-=(1.5/2.75))*t + .75); - } else if (t < (2.5/2.75)) { - return 1*(7.5625*(t-=(2.25/2.75))*t + .9375); - } else { - return 1*(7.5625*(t-=(2.625/2.75))*t + .984375); - } - }, - easeInOutBounce: function (t) { - if (t < 1/2) return animationOptions.easeInBounce (t*2) * .5; - return animationOptions.easeOutBounce (t*2-1) * .5 + 1*.5; - } - }; - - //Variables global to the chart - var width = context.canvas.width; - var height = context.canvas.height; - - - //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. - if (window.devicePixelRatio) { - context.canvas.style.width = width + "px"; - context.canvas.style.height = height + "px"; - context.canvas.height = height * window.devicePixelRatio; - context.canvas.width = width * window.devicePixelRatio; - context.scale(window.devicePixelRatio, window.devicePixelRatio); - } - - this.PolarArea = function(data,options){ - - chart.PolarArea.defaults = { - scaleOverlay : true, - scaleOverride : false, - scaleSteps : null, - scaleStepWidth : null, - scaleStartValue : null, - scaleShowLine : true, - scaleLineColor : "rgba(0,0,0,.1)", - scaleLineWidth : 1, - scaleShowLabels : true, - scaleLabel : "<%=value%>", - scaleFontFamily : "'Arial'", - scaleFontSize : 12, - scaleFontStyle : "normal", - scaleFontColor : "#666", - scaleShowLabelBackdrop : true, - scaleBackdropColor : "rgba(255,255,255,0.75)", - scaleBackdropPaddingY : 2, - scaleBackdropPaddingX : 2, - segmentShowStroke : true, - segmentStrokeColor : "#fff", - segmentStrokeWidth : 2, - animation : true, - animationSteps : 100, - animationEasing : "easeOutBounce", - animateRotate : true, - animateScale : false, - onAnimationComplete : null - }; - - var config = (options)? mergeChartConfig(chart.PolarArea.defaults,options) : chart.PolarArea.defaults; - - return new PolarArea(data,config,context); - }; - - this.Radar = function(data,options){ - - chart.Radar.defaults = { - scaleOverlay : false, - scaleOverride : false, - scaleSteps : null, - scaleStepWidth : null, - scaleStartValue : null, - scaleShowLine : true, - scaleLineColor : "rgba(0,0,0,.1)", - scaleLineWidth : 1, - scaleShowLabels : false, - scaleLabel : "<%=value%>", - scaleFontFamily : "'Arial'", - scaleFontSize : 12, - scaleFontStyle : "normal", - scaleFontColor : "#666", - scaleShowLabelBackdrop : true, - scaleBackdropColor : "rgba(255,255,255,0.75)", - scaleBackdropPaddingY : 2, - scaleBackdropPaddingX : 2, - angleShowLineOut : true, - angleLineColor : "rgba(0,0,0,.1)", - angleLineWidth : 1, - pointLabelFontFamily : "'Arial'", - pointLabelFontStyle : "normal", - pointLabelFontSize : 12, - pointLabelFontColor : "#666", - pointDot : true, - pointDotRadius : 3, - pointDotStrokeWidth : 1, - datasetStroke : true, - datasetStrokeWidth : 2, - datasetFill : true, - animation : true, - animationSteps : 60, - animationEasing : "easeOutQuart", - onAnimationComplete : null - }; - - var config = (options)? mergeChartConfig(chart.Radar.defaults,options) : chart.Radar.defaults; - - return new Radar(data,config,context); - }; - - this.Pie = function(data,options){ - chart.Pie.defaults = { - segmentShowStroke : true, - segmentStrokeColor : "#fff", - segmentStrokeWidth : 2, - animation : true, - animationSteps : 100, - animationEasing : "easeOutBounce", - animateRotate : true, - animateScale : false, - onAnimationComplete : null - }; - - var config = (options)? mergeChartConfig(chart.Pie.defaults,options) : chart.Pie.defaults; - - return new Pie(data,config,context); - }; - - this.Doughnut = function(data,options){ - - chart.Doughnut.defaults = { - segmentShowStroke : true, - segmentStrokeColor : "#fff", - segmentStrokeWidth : 2, - percentageInnerCutout : 50, - animation : true, - animationSteps : 100, - animationEasing : "easeOutBounce", - animateRotate : true, - animateScale : false, - onAnimationComplete : null - }; - - var config = (options)? mergeChartConfig(chart.Doughnut.defaults,options) : chart.Doughnut.defaults; - - return new Doughnut(data,config,context); - - }; - - this.Line = function(data,options){ - - chart.Line.defaults = { - scaleOverlay : false, - scaleOverride : false, - scaleSteps : null, - scaleStepWidth : null, - scaleStartValue : null, - scaleLineColor : "rgba(0,0,0,.1)", - scaleLineWidth : 1, - scaleShowLabels : true, - scaleLabel : "<%=value%>", - scaleFontFamily : "'Arial'", - scaleFontSize : 12, - scaleFontStyle : "normal", - scaleFontColor : "#666", - scaleShowGridLines : true, - scaleGridLineColor : "rgba(0,0,0,.05)", - scaleGridLineWidth : 1, - bezierCurve : true, - pointDot : true, - pointDotRadius : 4, - pointDotStrokeWidth : 2, - datasetStroke : true, - datasetStrokeWidth : 2, - datasetFill : true, - animation : true, - animationSteps : 60, - animationEasing : "easeOutQuart", - onAnimationComplete : null - }; - var config = (options) ? mergeChartConfig(chart.Line.defaults,options) : chart.Line.defaults; - - return new Line(data,config,context); - } - - this.Bar = function(data,options){ - chart.Bar.defaults = { - scaleOverlay : false, - scaleOverride : false, - scaleSteps : null, - scaleStepWidth : null, - scaleStartValue : null, - scaleLineColor : "rgba(0,0,0,.1)", - scaleLineWidth : 1, - scaleShowLabels : true, - scaleLabel : "<%=value%>", - scaleFontFamily : "'Arial'", - scaleFontSize : 12, - scaleFontStyle : "normal", - scaleFontColor : "#666", - scaleShowGridLines : true, - scaleGridLineColor : "rgba(0,0,0,.05)", - scaleGridLineWidth : 1, - barShowStroke : true, - barStrokeWidth : 2, - barValueSpacing : 5, - barDatasetSpacing : 1, - animation : true, - animationSteps : 60, - animationEasing : "easeOutQuart", - onAnimationComplete : null - }; - var config = (options) ? mergeChartConfig(chart.Bar.defaults,options) : chart.Bar.defaults; - - return new Bar(data,config,context); - } - - var clear = function(c){ - c.clearRect(0, 0, width, height); - }; - - var PolarArea = function(data,config,ctx){ - var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString; - - - calculateDrawingSizes(); - - valueBounds = getValueBounds(); - - labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : null; - - //Check and set the scale - if (!config.scaleOverride){ - - calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); - } - else { - calculatedScale = { - steps : config.scaleSteps, - stepValue : config.scaleStepWidth, - graphMin : config.scaleStartValue, - labels : [] - } - for (var i=0; i upperValue) {upperValue = data[i].value;} - if (data[i].value < lowerValue) {lowerValue = data[i].value;} - }; - - var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); - var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); - - return { - maxValue : upperValue, - minValue : lowerValue, - maxSteps : maxSteps, - minSteps : minSteps - }; - - - } - } - - var Radar = function (data,config,ctx) { - var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString; - - //If no labels are defined set to an empty array, so referencing length for looping doesn't blow up. - if (!data.labels) data.labels = []; - - calculateDrawingSizes(); - - var valueBounds = getValueBounds(); - - labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : null; - - //Check and set the scale - if (!config.scaleOverride){ - - calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); - } - else { - calculatedScale = { - steps : config.scaleSteps, - stepValue : config.scaleStepWidth, - graphMin : config.scaleStartValue, - labels : [] - } - for (var i=0; i Math.PI){ - ctx.textAlign = "right"; - } - else{ - ctx.textAlign = "left"; - } - - ctx.textBaseline = "middle"; - - ctx.fillText(data.labels[k],opposite,-adjacent); - - } - ctx.restore(); - }; - function calculateDrawingSizes(){ - maxSize = (Min([width,height])/2); - - labelHeight = config.scaleFontSize*2; - - var labelLength = 0; - for (var i=0; ilabelLength) labelLength = textMeasurement; - } - - //Figure out whats the largest - the height of the text or the width of what's there, and minus it from the maximum usable size. - maxSize -= Max([labelLength,((config.pointLabelFontSize/2)*1.5)]); - - maxSize -= config.pointLabelFontSize; - maxSize = CapValue(maxSize, null, 0); - scaleHeight = maxSize; - //If the label height is less than 5, set it to 5 so we don't have lines on top of each other. - labelHeight = Default(labelHeight,5); - }; - function getValueBounds() { - var upperValue = Number.MIN_VALUE; - var lowerValue = Number.MAX_VALUE; - - for (var i=0; i upperValue){upperValue = data.datasets[i].data[j]} - if (data.datasets[i].data[j] < lowerValue){lowerValue = data.datasets[i].data[j]} - } - } - - var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); - var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); - - return { - maxValue : upperValue, - minValue : lowerValue, - maxSteps : maxSteps, - minSteps : minSteps - }; - - - } - } - - var Pie = function(data,config,ctx){ - var segmentTotal = 0; - - //In case we have a canvas that is not a square. Minus 5 pixels as padding round the edge. - var pieRadius = Min([height/2,width/2]) - 5; - - for (var i=0; i 0){ - ctx.save(); - ctx.textAlign = "right"; - } - else{ - ctx.textAlign = "center"; - } - ctx.fillStyle = config.scaleFontColor; - for (var i=0; i 0){ - ctx.translate(yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize); - ctx.rotate(-(rotateLabels * (Math.PI/180))); - ctx.fillText(data.labels[i], 0,0); - ctx.restore(); - } - - else{ - ctx.fillText(data.labels[i], yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize+3); - } - - ctx.beginPath(); - ctx.moveTo(yAxisPosX + i * valueHop, xAxisPosY+3); - - //Check i isnt 0, so we dont go over the Y axis twice. - if(config.scaleShowGridLines && i>0){ - ctx.lineWidth = config.scaleGridLineWidth; - ctx.strokeStyle = config.scaleGridLineColor; - ctx.lineTo(yAxisPosX + i * valueHop, 5); - } - else{ - ctx.lineTo(yAxisPosX + i * valueHop, xAxisPosY+3); - } - ctx.stroke(); - } - - //Y axis - ctx.lineWidth = config.scaleLineWidth; - ctx.strokeStyle = config.scaleLineColor; - ctx.beginPath(); - ctx.moveTo(yAxisPosX,xAxisPosY+5); - ctx.lineTo(yAxisPosX,5); - ctx.stroke(); - - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - for (var j=0; j longestText)? measuredText : longestText; - } - //Add a little extra padding from the y axis - longestText +=10; - } - xAxisLength = width - longestText - widestXLabel; - valueHop = Math.floor(xAxisLength/(data.labels.length-1)); - - yAxisPosX = width-widestXLabel/2-xAxisLength; - xAxisPosY = scaleHeight + config.scaleFontSize/2; - } - function calculateDrawingSizes(){ - maxSize = height; - - //Need to check the X axis first - measure the length of each text metric, and figure out if we need to rotate by 45 degrees. - ctx.font = config.scaleFontStyle + " " + config.scaleFontSize+"px " + config.scaleFontFamily; - widestXLabel = 1; - for (var i=0; i widestXLabel)? textLength : widestXLabel; - } - if (width/data.labels.length < widestXLabel){ - rotateLabels = 45; - if (width/data.labels.length < Math.cos(rotateLabels) * widestXLabel){ - rotateLabels = 90; - maxSize -= widestXLabel; - } - else{ - maxSize -= Math.sin(rotateLabels) * widestXLabel; - } - } - else{ - maxSize -= config.scaleFontSize; - } - - //Add a little padding between the x line and the text - maxSize -= 5; - - - labelHeight = config.scaleFontSize; - - maxSize -= labelHeight; - //Set 5 pixels greater than the font size to allow for a little padding from the X axis. - - scaleHeight = maxSize; - - //Then get the area above we can safely draw on. - - } - function getValueBounds() { - var upperValue = Number.MIN_VALUE; - var lowerValue = Number.MAX_VALUE; - for (var i=0; i upperValue) { upperValue = data.datasets[i].data[j] }; - if ( data.datasets[i].data[j] < lowerValue) { lowerValue = data.datasets[i].data[j] }; - } - }; - - var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); - var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); - - return { - maxValue : upperValue, - minValue : lowerValue, - maxSteps : maxSteps, - minSteps : minSteps - }; - - - } - - - } - - var Bar = function(data,config,ctx){ - var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString, valueHop,widestXLabel, xAxisLength,yAxisPosX,xAxisPosY,barWidth, rotateLabels = 0; - - calculateDrawingSizes(); - - valueBounds = getValueBounds(); - //Check and set the scale - labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : ""; - if (!config.scaleOverride){ - - calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); - } - else { - calculatedScale = { - steps : config.scaleSteps, - stepValue : config.scaleStepWidth, - graphMin : config.scaleStartValue, - labels : [] - } - for (var i=0; i 0){ - ctx.save(); - ctx.textAlign = "right"; - } - else{ - ctx.textAlign = "center"; - } - ctx.fillStyle = config.scaleFontColor; - for (var i=0; i 0){ - ctx.translate(yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize); - ctx.rotate(-(rotateLabels * (Math.PI/180))); - ctx.fillText(data.labels[i], 0,0); - ctx.restore(); - } - - else{ - ctx.fillText(data.labels[i], yAxisPosX + i*valueHop + valueHop/2,xAxisPosY + config.scaleFontSize+3); - } - - ctx.beginPath(); - ctx.moveTo(yAxisPosX + (i+1) * valueHop, xAxisPosY+3); - - //Check i isnt 0, so we dont go over the Y axis twice. - ctx.lineWidth = config.scaleGridLineWidth; - ctx.strokeStyle = config.scaleGridLineColor; - ctx.lineTo(yAxisPosX + (i+1) * valueHop, 5); - ctx.stroke(); - } - - //Y axis - ctx.lineWidth = config.scaleLineWidth; - ctx.strokeStyle = config.scaleLineColor; - ctx.beginPath(); - ctx.moveTo(yAxisPosX,xAxisPosY+5); - ctx.lineTo(yAxisPosX,5); - ctx.stroke(); - - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - for (var j=0; j longestText)? measuredText : longestText; - } - //Add a little extra padding from the y axis - longestText +=10; - } - xAxisLength = width - longestText - widestXLabel; - valueHop = Math.floor(xAxisLength/(data.labels.length)); - - barWidth = (valueHop - config.scaleGridLineWidth*2 - (config.barValueSpacing*2) - (config.barDatasetSpacing*data.datasets.length-1) - ((config.barStrokeWidth/2)*data.datasets.length-1))/data.datasets.length; - - yAxisPosX = width-widestXLabel/2-xAxisLength; - xAxisPosY = scaleHeight + config.scaleFontSize/2; - } - function calculateDrawingSizes(){ - maxSize = height; - - //Need to check the X axis first - measure the length of each text metric, and figure out if we need to rotate by 45 degrees. - ctx.font = config.scaleFontStyle + " " + config.scaleFontSize+"px " + config.scaleFontFamily; - widestXLabel = 1; - for (var i=0; i widestXLabel)? textLength : widestXLabel; - } - if (width/data.labels.length < widestXLabel){ - rotateLabels = 45; - if (width/data.labels.length < Math.cos(rotateLabels) * widestXLabel){ - rotateLabels = 90; - maxSize -= widestXLabel; - } - else{ - maxSize -= Math.sin(rotateLabels) * widestXLabel; - } - } - else{ - maxSize -= config.scaleFontSize; - } - - //Add a little padding between the x line and the text - maxSize -= 5; - - - labelHeight = config.scaleFontSize; - - maxSize -= labelHeight; - //Set 5 pixels greater than the font size to allow for a little padding from the X axis. - - scaleHeight = maxSize; - - //Then get the area above we can safely draw on. - - } - function getValueBounds() { - var upperValue = Number.MIN_VALUE; - var lowerValue = Number.MAX_VALUE; - for (var i=0; i upperValue) { upperValue = data.datasets[i].data[j] }; - if ( data.datasets[i].data[j] < lowerValue) { lowerValue = data.datasets[i].data[j] }; - } - }; - - var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); - var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); - - return { - maxValue : upperValue, - minValue : lowerValue, - maxSteps : maxSteps, - minSteps : minSteps - }; - - - } - } - - function calculateOffset(val,calculatedScale,scaleHop){ - var outerValue = calculatedScale.steps * calculatedScale.stepValue; - var adjustedValue = val - calculatedScale.graphMin; - var scalingFactor = CapValue(adjustedValue/outerValue,1,0); - return (scaleHop*calculatedScale.steps) * scalingFactor; - } - - function animationLoop(config,drawScale,drawData,ctx){ - var animFrameAmount = (config.animation)? 1/CapValue(config.animationSteps,Number.MAX_VALUE,1) : 1, - easingFunction = animationOptions[config.animationEasing], - percentAnimComplete =(config.animation)? 0 : 1; - - - - if (typeof drawScale !== "function") drawScale = function(){}; - - requestAnimFrame(animLoop); - - function animateFrame(){ - var easeAdjustedAnimationPercent =(config.animation)? CapValue(easingFunction(percentAnimComplete),null,0) : 1; - clear(ctx); - if(config.scaleOverlay){ - drawData(easeAdjustedAnimationPercent); - drawScale(); - } else { - drawScale(); - drawData(easeAdjustedAnimationPercent); - } - } - function animLoop(){ - //We need to check if the animation is incomplete (less than 1), or complete (1). - percentAnimComplete += animFrameAmount; - animateFrame(); - //Stop the loop continuing forever - if (percentAnimComplete <= 1){ - requestAnimFrame(animLoop); - } - else{ - if (typeof config.onAnimationComplete == "function") config.onAnimationComplete(); - } - - } - - } - - //Declare global functions to be called within this namespace here. - - - // shim layer with setTimeout fallback - var requestAnimFrame = (function(){ - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback) { - window.setTimeout(callback, 1000 / 60); - }; - })(); - - function calculateScale(drawingHeight,maxSteps,minSteps,maxValue,minValue,labelTemplateString){ - var graphMin,graphMax,graphRange,stepValue,numberOfSteps,valueRange,rangeOrderOfMagnitude,decimalNum; - - valueRange = maxValue - minValue; - - rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange); - - graphMin = Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude); - - graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude); - - graphRange = graphMax - graphMin; - - stepValue = Math.pow(10, rangeOrderOfMagnitude); - - numberOfSteps = Math.round(graphRange / stepValue); - - //Compare number of steps to the max and min for that size graph, and add in half steps if need be. - while(numberOfSteps < minSteps || numberOfSteps > maxSteps) { - if (numberOfSteps < minSteps){ - stepValue /= 2; - numberOfSteps = Math.round(graphRange/stepValue); - } - else{ - stepValue *=2; - numberOfSteps = Math.round(graphRange/stepValue); - } - }; - - - - //Create an array of all the labels by interpolating the string. - - var labels = []; - - if(labelTemplateString){ - //Fix floating point errors by setting to fixed the on the same decimal as the stepValue. - for (var i=1; i maxValue ) { - return maxValue; - } - } - if(isNumber(minValue)){ - if ( valueToCap < minValue ){ - return minValue; - } - } - return valueToCap; - } - function getDecimalPlaces (num){ - var numberOfDecimalPlaces; - if (num%1!=0){ - return num.toString().split(".")[1].length - } - else{ - return 0; - } - - } - - function mergeChartConfig(defaults,userDefined){ - var returnObj = {}; - for (var attrname in defaults) { returnObj[attrname] = defaults[attrname]; } - for (var attrname in userDefined) { returnObj[attrname] = userDefined[attrname]; } - return returnObj; - } - - //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ - var cache = {}; - - function tmpl(str, data){ - // Figure out if we're getting a template, or if we need to - // load the template - and be sure to cache the result. - var fn = !/\W/.test(str) ? - cache[str] = cache[str] || - tmpl(document.getElementById(str).innerHTML) : - - // Generate a reusable function that will serve as a template - // generator (and which will be cached). - new Function("obj", - "var p=[],print=function(){p.push.apply(p,arguments);};" + - - // Introduce the data as local variables using with(){} - "with(obj){p.push('" + - - // Convert the template into pure JavaScript - str - .replace(/[\r\t\n]/g, " ") - .split("<%").join("\t") - .replace(/((^|%>)[^\t]*)'/g, "$1\r") - .replace(/\t=(.*?)%>/g, "',$1,'") - .split("\t").join("');") - .split("%>").join("p.push('") - .split("\r").join("\\'") - + "');}return p.join('');"); - - // Provide some basic currying to the user - return data ? fn( data ) : fn; - }; -} - - - diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index ef6f522c357..00000000000 --- a/docs/index.html +++ /dev/null @@ -1,766 +0,0 @@ - - - - Chart.js Documentation - - - - - - - - -
-
-
-
- - -
-

Chart.js Documentation

-

Everything you need to know to create great looking charts using Chart.js

-
-

Getting started

-

Include Chart.js

-

First we need to include the Chart.js library on the page. The library occupies a global variable of Chart.

-
<script src="Chart.js"></script>
-

Creating a chart

-

To create a chart, we need to instantiate the Chart class. To do this, we need to pass in the 2d context of where we want to draw the chart. Here's an example.

-
<canvas id="myChart" width="400" height="400"></canvas>
-
//Get the context of the canvas element we want to select
-var ctx = document.getElementById("myChart").getContext("2d");
-var myNewChart = new Chart(ctx).PolarArea(data);
-

We can also get the context of our canvas with jQuery. To do this, we need to get the DOM node out of the jQuery collection, and call the getContext("2d") method on that.

-
//Get context with jQuery - using jQuery's .get() method.
-var ctx = $("#myChart").get(0).getContext("2d");
-//This will get the first returned node in the jQuery collection.
-var myNewChart = new Chart(ctx);
-

After we've instantiated the Chart class on the canvas we want to draw on, Chart.js will handle the scaling for retina displays.

-

With the Chart class set up, we can go on to create one of the charts Chart.js has available. In the example below, we would be drawing a Polar area chart.

-
new Chart(ctx).PolarArea(data,options);
-

We call a method of the name of the chart we want to create. We pass in the data for that chart type, and the options for that chart as parameters. Chart.js will merge the options you pass in with the default options for that chart type.

-
- -
-

Line chart

-

Introduction

-

A line chart is a way of plotting data points on a line.

-

Often, it is used to show trend data, and the comparison of two data sets.

-

Example usage

- -
new Chart(ctx).Line(data,options);
-

Data structure

-
var data = {
-	labels : ["January","February","March","April","May","June","July"],
-	datasets : [
-		{
-			fillColor : "rgba(220,220,220,0.5)",
-			strokeColor : "rgba(220,220,220,1)",
-			pointColor : "rgba(220,220,220,1)",
-			pointStrokeColor : "#fff",
-			data : [65,59,90,81,56,55,40]
-		},
-		{
-			fillColor : "rgba(151,187,205,0.5)",
-			strokeColor : "rgba(151,187,205,1)",
-			pointColor : "rgba(151,187,205,1)",
-			pointStrokeColor : "#fff",
-			data : [28,48,40,19,96,27,100]
-		}
-	]
-}
-

The line chart requires an array of labels for each of the data points. This is show on the X axis.

-

The data for line charts is broken up into an array of datasets. Each dataset has a colour for the fill, a colour for the line and colours for the points and strokes of the points. These colours are strings just like CSS. You can use RGBA, RGB, HEX or HSL notation.

- -

Chart options

-
Line.defaults = {
-				
-	//Boolean - If we show the scale above the chart data			
-	scaleOverlay : false,
-	
-	//Boolean - If we want to override with a hard coded scale
-	scaleOverride : false,
-	
-	//** Required if scaleOverride is true **
-	//Number - The number of steps in a hard coded scale
-	scaleSteps : null,
-	//Number - The value jump in the hard coded scale
-	scaleStepWidth : null,
-	//Number - The scale starting value
-	scaleStartValue : null,
-
-	//String - Colour of the scale line	
-	scaleLineColor : "rgba(0,0,0,.1)",
-	
-	//Number - Pixel width of the scale line	
-	scaleLineWidth : 1,
-
-	//Boolean - Whether to show labels on the scale	
-	scaleShowLabels : true,
-	
-	//Interpolated JS string - can access value
-	scaleLabel : "<%=value%>",
-	
-	//String - Scale label font declaration for the scale label
-	scaleFontFamily : "'Arial'",
-	
-	//Number - Scale label font size in pixels	
-	scaleFontSize : 12,
-	
-	//String - Scale label font weight style	
-	scaleFontStyle : "normal",
-	
-	//String - Scale label font colour	
-	scaleFontColor : "#666",	
-	
-	///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 the line is curved between points
-	bezierCurve : true,
-	
-	//Boolean - Whether to show a dot for each point
-	pointDot : true,
-	
-	//Number - Radius of each point dot in pixels
-	pointDotRadius : 3,
-	
-	//Number - Pixel width of point dot stroke
-	pointDotStrokeWidth : 1,
-	
-	//Boolean - Whether to show a stroke for datasets
-	datasetStroke : true,
-	
-	//Number - Pixel width of dataset stroke
-	datasetStrokeWidth : 2,
-	
-	//Boolean - Whether to fill the dataset with a colour
-	datasetFill : true,
-	
-	//Boolean - Whether to animate the chart
-	animation : true,
-
-	//Number - Number of animation steps
-	animationSteps : 60,
-	
-	//String - Animation easing effect
-	animationEasing : "easeOutQuart",
-
-	//Function - Fires when the animation is complete
-	onAnimationComplete : null
-	
-}
-
- -
-

Bar chart

-

Introduction

-

A bar chart is a way of showing data as bars.

-

It is sometimes used to show trend data, and the comparison of multiple data sets side by side.

-

Example usage

- -
new Chart(ctx).Bar(data,options);
-

Data structure

-
var data = {
-	labels : ["January","February","March","April","May","June","July"],
-	datasets : [
-		{
-			fillColor : "rgba(220,220,220,0.5)",
-			strokeColor : "rgba(220,220,220,1)",
-			data : [65,59,90,81,56,55,40]
-		},
-		{
-			fillColor : "rgba(151,187,205,0.5)",
-			strokeColor : "rgba(151,187,205,1)",
-			data : [28,48,40,19,96,27,100]
-		}
-	]
-}
-

The bar chart has the a very similar data structure to the line chart, and has an array of datasets, each with colours and an array of data. Again, colours are in CSS format.

-

We have an array of labels too for display. In the example, we are showing the same data as the previous line chart example.

- -

Chart options

-
Bar.defaults = {
-				
-	//Boolean - If we show the scale above the chart data			
-	scaleOverlay : false,
-	
-	//Boolean - If we want to override with a hard coded scale
-	scaleOverride : false,
-	
-	//** Required if scaleOverride is true **
-	//Number - The number of steps in a hard coded scale
-	scaleSteps : null,
-	//Number - The value jump in the hard coded scale
-	scaleStepWidth : null,
-	//Number - The scale starting value
-	scaleStartValue : null,
-
-	//String - Colour of the scale line	
-	scaleLineColor : "rgba(0,0,0,.1)",
-	
-	//Number - Pixel width of the scale line	
-	scaleLineWidth : 1,
-
-	//Boolean - Whether to show labels on the scale	
-	scaleShowLabels : true,
-	
-	//Interpolated JS string - can access value
-	scaleLabel : "<%=value%>",
-	
-	//String - Scale label font declaration for the scale label
-	scaleFontFamily : "'Arial'",
-	
-	//Number - Scale label font size in pixels	
-	scaleFontSize : 12,
-	
-	//String - Scale label font weight style	
-	scaleFontStyle : "normal",
-	
-	//String - Scale label font colour	
-	scaleFontColor : "#666",	
-	
-	///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 - 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,
-	
-	//Boolean - Whether to animate the chart
-	animation : true,
-
-	//Number - Number of animation steps
-	animationSteps : 60,
-	
-	//String - Animation easing effect
-	animationEasing : "easeOutQuart",
-
-	//Function - Fires when the animation is complete
-	onAnimationComplete : null
-	
-}
-
- -
-

Radar chart

-

Introduction

-

A radar chart is a way of showing multiple data points and the variation between them.

-

They are often useful for comparing the points of two or more different data sets

-

Example usage

- -
new Chart(ctx).Radar(data,options);
-

Data structure

-
var data = {
-	labels : ["Eating","Drinking","Sleeping","Designing","Coding","Partying","Running"],
-	datasets : [
-		{
-			fillColor : "rgba(220,220,220,0.5)",
-			strokeColor : "rgba(220,220,220,1)",
-			pointColor : "rgba(220,220,220,1)",
-			pointStrokeColor : "#fff",
-			data : [65,59,90,81,56,55,40]
-		},
-		{
-			fillColor : "rgba(151,187,205,0.5)",
-			strokeColor : "rgba(151,187,205,1)",
-			pointColor : "rgba(151,187,205,1)",
-			pointStrokeColor : "#fff",
-			data : [28,48,40,19,96,27,100]
-		}
-	]
-}
-

For a radar chart, usually you will want to show a label on each point of the chart, so we include an array of strings that we show around each point in the chart. If you do not want this, you can either not include the array of labels, or choose to hide them in the chart options.

-

For the radar chart data, we have an array of datasets. Each of these is an object, with a fill colour, a stroke colour, a colour for the fill of each point, and a colour for the stroke of each point. We also have an array of data values.

- -

Chart options

-
Radar.defaults = {
-				
-	//Boolean - If we show the scale above the chart data			
-	scaleOverlay : false,
-	
-	//Boolean - If we want to override with a hard coded scale
-	scaleOverride : false,
-	
-	//** Required if scaleOverride is true **
-	//Number - The number of steps in a hard coded scale
-	scaleSteps : null,
-	//Number - The value jump in the hard coded scale
-	scaleStepWidth : null,
-	//Number - The centre starting value
-	scaleStartValue : null,
-	
-	//Boolean - Whether to show lines for each scale point
-	scaleShowLine : true,
-
-	//String - Colour of the scale line	
-	scaleLineColor : "rgba(0,0,0,.1)",
-	
-	//Number - Pixel width of the scale line	
-	scaleLineWidth : 1,
-
-	//Boolean - Whether to show labels on the scale	
-	scaleShowLabels : false,
-	
-	//Interpolated JS string - can access value
-	scaleLabel : "<%=value%>",
-	
-	//String - Scale label font declaration for the scale label
-	scaleFontFamily : "'Arial'",
-	
-	//Number - Scale label font size in pixels	
-	scaleFontSize : 12,
-	
-	//String - Scale label font weight style	
-	scaleFontStyle : "normal",
-	
-	//String - Scale label font colour	
-	scaleFontColor : "#666",
-	
-	//Boolean - Show a backdrop to the scale label
-	scaleShowLabelBackdrop : true,
-	
-	//String - The colour of the label backdrop	
-	scaleBackdropColor : "rgba(255,255,255,0.75)",
-	
-	//Number - The backdrop padding above & below the label in pixels
-	scaleBackdropPaddingY : 2,
-	
-	//Number - The backdrop padding to the side of the label in pixels	
-	scaleBackdropPaddingX : 2,
-	
-	//Boolean - Whether we show the angle lines out of the radar
-	angleShowLineOut : true,
-	
-	//String - Colour of the angle line
-	angleLineColor : "rgba(0,0,0,.1)",
-	
-	//Number - Pixel width of the angle line
-	angleLineWidth : 1,			
-	
-	//String - Point label font declaration
-	pointLabelFontFamily : "'Arial'",
-	
-	//String - Point label font weight
-	pointLabelFontStyle : "normal",
-	
-	//Number - Point label font size in pixels	
-	pointLabelFontSize : 12,
-	
-	//String - Point label font colour	
-	pointLabelFontColor : "#666",
-	
-	//Boolean - Whether to show a dot for each point
-	pointDot : true,
-	
-	//Number - Radius of each point dot in pixels
-	pointDotRadius : 3,
-	
-	//Number - Pixel width of point dot stroke
-	pointDotStrokeWidth : 1,
-	
-	//Boolean - Whether to show a stroke for datasets
-	datasetStroke : true,
-	
-	//Number - Pixel width of dataset stroke
-	datasetStrokeWidth : 2,
-	
-	//Boolean - Whether to fill the dataset with a colour
-	datasetFill : true,
-	
-	//Boolean - Whether to animate the chart
-	animation : true,
-
-	//Number - Number of animation steps
-	animationSteps : 60,
-	
-	//String - Animation easing effect
-	animationEasing : "easeOutQuart",
-
-	//Function - Fires when the animation is complete
-	onAnimationComplete : null
-	
-}
-
- -
-

Polar area chart

-

Introduction

-

Polar area charts are similar to pie charts, but each segment has the same angle - the radius of the segment differs depending on the value.

-

This type of chart is often useful when we want to show a comparison data similar to a pie chart, but also show a scale of values for context.

-

Example usage

- -
new Chart(ctx).PolarArea(data,options);
-

Data structure

-
var data = [
-	{
-		value : 30,
-		color: "#D97041"
-	},
-	{
-		value : 90,
-		color: "#C7604C"
-	},
-	{
-		value : 24,
-		color: "#21323D"
-	},
-	{
-		value : 58,
-		color: "#9D9B7F"
-	},
-	{
-		value : 82,
-		color: "#7D4F6D"
-	},
-	{
-		value : 8,
-		color: "#584A5E"
-	}
-]
-

As you can see, for the chart data you pass in an array of objects, with a value and a colour. The value attribute should be a number, while the color attribute should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL.

-

Chart options

-

These are the default chart options. By passing in an object with any of these attributes, Chart.js will merge these objects and the graph accordingly. Explanations of each option are commented in the code below.

-
PolarArea.defaults = {
-				
-	//Boolean - Whether we show the scale above or below the chart segments
-	scaleOverlay : true,
-	
-	//Boolean - If we want to override with a hard coded scale
-	scaleOverride : false,
-	
-	//** Required if scaleOverride is true **
-	//Number - The number of steps in a hard coded scale
-	scaleSteps : null,
-	//Number - The value jump in the hard coded scale
-	scaleStepWidth : null,
-	//Number - The centre starting value
-	scaleStartValue : null,
-	
-	//Boolean - Show line for each value in the scale
-	scaleShowLine : true,
-	
-	//String - The colour of the scale line
-	scaleLineColor : "rgba(0,0,0,.1)",
-	
-	//Number - The width of the line - in pixels
-	scaleLineWidth : 1,
-	
-	//Boolean - whether we should show text labels
-	scaleShowLabels : true,
-	
-	//Interpolated JS string - can access value
-	scaleLabel : "<%=value%>",
-	
-	//String - Scale label font declaration for the scale label
-	scaleFontFamily : "'Arial'",
-	
-	//Number - Scale label font size in pixels	
-	scaleFontSize : 12,
-	
-	//String - Scale label font weight style	
-	scaleFontStyle : "normal",
-	
-	//String - Scale label font colour	
-	scaleFontColor : "#666",
-	
-	//Boolean - Show a backdrop to the scale label
-	scaleShowLabelBackdrop : true,
-	
-	//String - The colour of the label backdrop	
-	scaleBackdropColor : "rgba(255,255,255,0.75)",
-	
-	//Number - The backdrop padding above & below the label in pixels
-	scaleBackdropPaddingY : 2,
-	
-	//Number - The backdrop padding to the side of the label in pixels	
-	scaleBackdropPaddingX : 2,
-
-	//Boolean - Stroke a line around each segment in the chart
-	segmentShowStroke : true,
-	
-	//String - The colour of the stroke on each segement.
-	segmentStrokeColor : "#fff",
-	
-	//Number - The width of the stroke value in pixels	
-	segmentStrokeWidth : 2,
-	
-	//Boolean - Whether to animate the chart or not
-	animation : true,
-	
-	//Number - Amount of animation steps
-	animationSteps : 100,
-	
-	//String - Animation easing effect.
-	animationEasing : "easeOutBounce",
-
-	//Boolean - Whether to animate the rotation of the chart
-	animateRotate : true,
-	
-	//Boolean - Whether to animate scaling the chart from the centre
-	animateScale : false,
-
-	//Function - This will fire when the animation of the chart is complete.
-	onAnimationComplete : null
-}
-
-
-

Pie chart

-

Introduction

-

Pie charts are probably the most commonly used chart there are. They are divided into segments, the arc of each segment shows a the proportional value of each piece of data.

-

They are excellent at showing the relational proportions between data.

-

Example usage

- -
new Chart(ctx).Pie(data,options);
-

Data structure

-
var data = [
-	{
-		value: 30,
-		color:"#F38630"
-	},
-	{
-		value : 50,
-		color : "#E0E4CC"
-	},
-	{
-		value : 100,
-		color : "#69D2E7"
-	}			
-]
-

For a pie chart, you must pass in an array of objects with a value and a color property. The value attribute should be a number, Chart.js will total all of the numbers and calculate the relative proportion of each. The color attribute should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL.

-

Chart options

-

These are the default options for the Pie chart. Pass in an object with any of these attributes to override them. -

Pie.defaults = {
-	//Boolean - Whether we should show a stroke on each segment
-	segmentShowStroke : true,
-	
-	//String - The colour of each segment stroke
-	segmentStrokeColor : "#fff",
-	
-	//Number - The width of each segment stroke
-	segmentStrokeWidth : 2,
-	
-	//Boolean - Whether we should animate the chart	
-	animation : true,
-	
-	//Number - Amount of animation steps
-	animationSteps : 100,
-	
-	//String - Animation easing effect
-	animationEasing : "easeOutBounce",
-	
-	//Boolean - Whether we animate the rotation of the Pie
-	animateRotate : true,
-
-	//Boolean - Whether we animate scaling the Pie from the centre
-	animateScale : false,
-	
-	//Function - Will fire on animation completion.
-	onAnimationComplete : null
-}
-
-
-

Doughnut chart

-

Introduction

-

Doughnut charts are similar to pie charts, however they have the centre cut out, and are therefore shaped more like a doughnut than a pie!

-

They are aso excellent at showing the relational proportions between data.

-

Example usage

- -
new Chart(ctx).Doughnut(data,options);
-

Data structure

-
var data = [
-	{
-		value: 30,
-		color:"#F7464A"
-	},
-	{
-		value : 50,
-		color : "#E2EAE9"
-	},
-	{
-		value : 100,
-		color : "#D4CCC5"
-	},
-	{
-		value : 40,
-		color : "#949FB1"
-	},
-	{
-		value : 120,
-		color : "#4D5360"
-	}
-
-]
-

For a doughnut chart, you must pass in an array of objects with a value and a color property. The value attribute should be a number, Chart.js will total all of the numbers and calculate the relative proportion of each. The color attribute should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL.

-

Chart options

-

These are the default options for the doughnut chart. Pass in an object with any of these attributes to override them. -

Doughnut.defaults = {
-	//Boolean - Whether we should show a stroke on each segment
-	segmentShowStroke : true,
-	
-	//String - The colour of each segment stroke
-	segmentStrokeColor : "#fff",
-	
-	//Number - The width of each segment stroke
-	segmentStrokeWidth : 2,
-	
-	//The percentage of the chart that we cut out of the middle.
-	percentageInnerCutout : 50,
-	
-	//Boolean - Whether we should animate the chart	
-	animation : true,
-	
-	//Number - Amount of animation steps
-	animationSteps : 100,
-	
-	//String - Animation easing effect
-	animationEasing : "easeOutBounce",
-	
-	//Boolean - Whether we animate the rotation of the Doughnut
-	animateRotate : true,
-
-	//Boolean - Whether we animate scaling the Doughnut from the centre
-	animateScale : false,
-	
-	//Function - Will fire on animation completion.
-	onAnimationComplete : null
-}
-
-
-

General issues

-

Chart interactivity

-

If you are looking to add interaction as a layer to charts, Chart.js is not the library for you. A better option would be using SVG, as this will let you attach event listeners to any of the elements in the chart, as these are all DOM nodes.

-

Chart.js uses the canvas element, which is a single DOM node, similar in characteristics to a static image. This does mean that it has a wider scope for compatibility, and less memory implications than SVG based charting solutions. The canvas element also allows for saving the contents as a base 64 string, allowing saving the chart as an image.

-

In SVG, all of the lines, data points and everything you see is a DOM node. As a result of this, complex charts with a lot of intricacies, or many charts on the page will often see dips in performance when scrolling or generating the chart, especially when there are multiple on the page. SVG also has relatively poor mobile support, with Android not supporting SVG at all before version 3.0, and iOS before 5.0. (caniuse.com/svg-html5).

-

Browser support

-

Browser support for the canvas element is available in all modern & major mobile browsers (caniuse.com/canvas).

-

For IE8 & below, I would recommend using the polyfill ExplorerCanvas - available at https://code.google.com/p/explorercanvas/. It falls back to Internet explorer's format VML when canvas support is not available. Example use:

-
<head>
-	<!--[if lte IE 8]>
-		<script src="excanvas.js"></script>
-	<![endif]-->
-</head>
-

Usually I would recommend feature detection to choose whether or not to load a polyfill, rather than IE conditional comments, however in this case, VML is a Microsoft proprietary format, so it will only work in IE.

-

Some important points to note in my experience using ExplorerCanvas as a fallback.

-
    -
  • Initialise charts on load rather than DOMContentReady when using the library, as sometimes a race condition will occur, and it will result in an error when trying to get the 2d context of a canvas.
  • -
  • New VML DOM elements are being created for each animation frame and there is no hardware acceleration. As a result animation is usually slow and jerky, with flashing text. It is a good idea to dynamically turn off animation based on canvas support. I recommend using the excellent Modernizr to do this.
  • -
  • When declaring fonts, the library explorercanvas requires the font name to be in single quotes inside the string. For example, instead of your scaleFontFamily property being simply "Arial", explorercanvas support, use "'Arial'" instead. Chart.js does this for default values.
  • - -
-

Bugs & issues

-

Please report these on the Github page - at github.com/nnnick/Chart.js.

-

New contributions to the library are welcome.

-

License

-

Chart.js is open source and available under the MIT license.

-
-
-
- - - - diff --git a/docs/prettify.css b/docs/prettify.css deleted file mode 100644 index 22e0d02f3f4..00000000000 --- a/docs/prettify.css +++ /dev/null @@ -1,38 +0,0 @@ -/* Pretty printing styles. Used with prettify.js. */ -/* Vim sunburst theme by David Leibovic */ - -pre .str, code .str { color: #65B042; } /* string - green */ -pre .kwd, code .kwd { color: #E28964; } /* keyword - dark pink */ -pre .com, code .com { color: #AEAEAE; font-style: italic; } /* comment - gray */ -pre .typ, code .typ { color: #89bdff; } /* type - light blue */ -pre .lit, code .lit { color: #3387CC; } /* literal - blue */ -pre .pun, code .pun { color: #fff; } /* punctuation - white */ -pre .pln, code .pln { color: #fff; } /* plaintext - white */ -pre .tag, code .tag { color: #89bdff; } /* html/xml tag - light blue */ -pre .atn, code .atn { color: #bdb76b; } /* html/xml attribute name - khaki */ -pre .atv, code .atv { color: #65B042; } /* html/xml attribute value - green */ -pre .dec, code .dec { color: #3387CC; } /* decimal - blue */ - -pre.prettyprint, code.prettyprint { - background-color: #000; - -moz-border-radius: 8px; - -webkit-border-radius: 8px; - -o-border-radius: 8px; - -ms-border-radius: 8px; - -khtml-border-radius: 8px; - border-radius: 8px; -} - -pre.prettyprint { - width: 95%; - margin: 1em auto; - padding: 1em; - white-space: pre-wrap; -} - - -/* Specify class=linenums on a pre to get line numbering */ -ol.linenums { margin-top: 0; margin-bottom: 0; color: #AEAEAE; } /* IE indents via margin-left */ -li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8 { list-style-type: none } -/* Alternate shading for lines */ -li.L1,li.L3,li.L5,li.L7,li.L9 { } diff --git a/docs/prettify.js b/docs/prettify.js deleted file mode 100644 index 0f7ffa0c97e..00000000000 --- a/docs/prettify.js +++ /dev/null @@ -1,28 +0,0 @@ -var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; -(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= -[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), -l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, -q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, -q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, -"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), -a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} -for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], -"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], -H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], -J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ -I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), -["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", -/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), -["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", -hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}p - - - Polar Area Chart - - - - - - - - - - - diff --git a/samples/sixup.html b/samples/sixup.html deleted file mode 100644 index 37b957e72fa..00000000000 --- a/samples/sixup.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - Doughnut Chart - - - - - -
- - - - - - -
- - - - - diff --git a/site/assets/6charts.png b/site/assets/6charts.png deleted file mode 100644 index 17dc2dd1e417dbfb6ce0d4fece0c79adf9bab3e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79923 zcmaHSbC@T+wr(5K_Oxx=wtL#PjbGcgZQHh|HEr9rtvla7XP z&d`KF(#F8dMA5{+*xg~wga-%+l*B?=9iT2N!)0V=Luc@h44s>e{a-W?5D&kby@8RH z34p-R#LU8$m*~2)mx#c^n3qV6RhB{4Uf9IkLc+t*M9D)=*~r7nh|`#epO1jYjq5Lg zjS0Yjz|F?m)``oFm*`)-Tz|v=B-0ZS{7VF2#Y^;GMybmx5D42jnh>zkG1D3`FftOb zvC%QIu(Gi+(hx8)Ff!3I{5{!d8Ckek*|?b43I6*a`m4>+*py3AMC`wH{f+SwnF9d! zT=ev=uC8>h%yf2+X7r4loSgq~Ffq~om7sNUw*?ru(b_r@|C>R?#L39f!X98@XG`!8 zqk*BFGk}-quciN$1snVSp|y4T?_&B}VDxSV_VkQ&4F6>5--5ET|9?>%oBxq^0w|jN zuf6|IVkc#HdlPy^6DK=oN29+LXG;8!Dtj(rM-u~poujgyo%O#IDkI9j-v7>haD z*%16I(_9w+*%rb7$oJn^O!22!d(7d4aU zOp6O+v)d*mpX&RM8Z*@w2`@_sUFE8W@iD*kBv#ztzS-DQoUj(F;$~T93CFV z&494!;W8cMM+dyUz2(W3Mo;_P^D1bX8ylgta!h7T$DH2f?J4h+9i@eYgpeGXCL$3r zrYOw^k?}n1fKFGGx1~iyMv^|)qhgEnI5ZU%1%1+$eVWXzdVC#StTxtpzq_PmXD4_6 z3>OLdSXud*x7(~vBh=m|Co2o5-ObsL{vzhYh3W|5Gnv6^ZDCPf1nW}BYcrMbnjfbBpe`8nU`|0cgI+T_U2O+zo6pbUFudgur z%&{rd^SO>R=4p=bwfP3e01FzL`3>Rw_Q~xOfxrYJ4~w9AvyG{xg$B7%SbZGq<>bVy zW1{ZCZv>GTXV=Ib{1LWH-w3DG$_rAYx#;^S#e-QG?|vy*}Z? z5c7`wJg_guYQaDJzHMk9URaME(0~Za0`)wh4%$o+p&G`I4dwpPRdm16}I_x+_5Uy34 zAQvqfD0+P{4wRLNa^lg%p$Ja}Rv=3U{ zRnVHZu=%~yJ0XY(88CnBydJ3$uDKv<>t&;F&Gx=P0%D`cn!oj2YuR>m#1BlMcgp=? znXt_4q>QO)Ww3hH6)Aw@G4vNhG$w~(^A+Hd zeR^-k?qXo1TX+3{)#T%YjOj=6py%2GZ@a;cpd$rMOC*;k!!|7HtJ%r`Ne*b;we5Bd>m4`_;Bpi)GijNQU$fP;e* z`U+&@uF^}vxi+7#!LW-kcMOb5N&SlkeH__~K-NeeClT$|C#Pvj!>(ze{1pH4Q~Q{qn2&qw z$Zvi*H0WKKvQwMxOc@iDkS}LPfQlynmoz8ZbdbW{FtUb&k&zLPb#~hBLTZXVh5qpm zSdZ1l;gIG!t9#2`zGTHgn<61(YXgEHxQ?fH&P^+fn@>ak zbqz&ud~5f?_gihHYt0(=V@IdaR}0wo^0Mpki@P?TTy(ViV2DZCQDY;p?b;}>91W_^7`5$3x||20v!(nXW|wkM z;z_Mpy6%oB+9!BHfu(d*miX80Oc`@`=Xxy8;)JD2@A}rZ9rFvk0jy`m`#QD5j)K|eDI+{any4ve53w&KP}Y~gA8#3utMja zCWg(@%e9OK2L~H2Em+5xZCf+PMpoYqc|i;1yOS5$As`^ytOh~Z)NQqppFuzDNowv5-5^9xRRB@K7ar>3kaby_Tk$~uQ_=9(fPtj`r zA->|z;#|FUJ;Z}Yp}p_^Fr4ljC#O^HIVG(bg83~l6OfvXa%s;daD0VbK!H%C`r5Rt zy!`P)ZszM+jS$4B4-MDXXB)6rxQrVJs*#_Q4;U{#mE5H<-dE+I-k%7j>Lja~royJz zl`aHc?Si+yW6l5 z3^Y0a4X%IvW>itJ_u|fjcm0Yde*g|eL@3C}ij=-P!S<)Rx;-hwxIIHyLn}Hu#b|ue zvgL4CwQ}g~HdHp#sk+`Yy?}GQp;#kGRw>Wo1r%=w^w$F9i{G>EvF8eg2Gb&qR%u2avkSMTxrr%#W1Nt#ej9hr_@36&fM6(uD!c?Jtl@?MUAKl}_Y2QDy%MjEN7d&cLDOLuqo-FHkkdLTM# zYNHLgL^mG1SVX1-y1IT;vyDiI-{3C=qLKPZP75`Sg_95*(=HBL8`oc50@o<5@DdTB z>cZAhb3=Xfbl$6%F8)jq{lY)pUp|(7yAGH`Y16X;?@_j{C;kXD39+VJJZ0)=d&nU=!%i@N1r0th+>;R}7c3JXEI`joE9R_IM68>{p4 z`){KC9O?-ojda>@4I!N4R@dmX+Zf39@ELCg7L+0f+wfX}f3dQF6k}{G$i2uP(mb|>( z;LQR1-vTw1azNqp4JMkHe_4TqDpD&rieA2rm8y>Oottr#wYRqqFh-%4(b76B?=?tb zV*_jMO^9H#J7H=FR>2#@;Y|{m7}8~c%l+E1d+EW-NQcI4ku?1akfNPOUUlk1~kkF~?X* z$@X#LCuJSHQHx~#o@fZTOsv&C!hUH#+k<`V+omHPMl=-Iv$VAIYq0FcB1kOErA>Pf zTe1_SZnb(*qawL952~p>YrUfbx&P7VL=q%fKNr|#zAC|J5}sOfi8y4Fps-ep3P4js zqnyLnf;#gWDaJEI3{_OG))>}Ll?n(k(Z=G-Yh&GG)d1Ot+ugjXRnR8;3Qt6pY&Wi= z1+jK%CplD9Gs8{dk&A^kpf{Q^SMXyOhYOTs;3llJXk|u3p=;OO&tC z`hYN@URSlyZ4eLV!6n5rYkEDVy@-=S{Z!=A`Qe?lw=`9_RCz?`{JsqwwaLX`ZpkrU%NMQX_tH{C<>Au8g7Jwb zu|H$d3A)-2Hk-A;vt?$z@)hQ|PAFnUU(R+wjhH*`OO~mv6KWCMCs@N5KaGEXkj`3npxxi^O5yy_>ZF#MFa8iZVMI*x@Bi(C|Gd!Rk*JRL4U?i(pJtn;*vuosI0})4 ziT@$^Dv3Bm*$O#VP2Md~Dkbf=L?sWsBo^OX>`)rnYAIx8fh{C$aBrvz7%03|B^yI zF8=HbW`Q%??W56v-@SOSf?R)kLykD6$;u>ye4dwc1lU6+FlhwCxABpo1va`{#YFks z2o6(9L-oP%*c#g|Au32ot{Pda-U2v(E7k?>8x=?ap+G>lJ#O00XNxW)W3d!bk_ZpF zg7@*jtRhcV*jiXw1*V2s!jVx+_qG^1B{$FOc`{6wM>a#P& zJ==<9BZ5^s{6waU)Uw!gPS zl=5a&z3)2OAnf%Yp-!{<#rNKvZnO{GPH|g=h(FbBsVU*HAiD4MN7=X~oDu06_2^JI-5V@mJu(`o&MJK)*$e0#M|NVsN zu<+4T(AJJR0hy+%t*Q!Yj5Fw~{xCDKVs zmf0Fxwr_ZpA@t5F=OiZ&ZD?qac}@$iMdWiIb>yI4EJRtXn@JYz4r3`kA_|zVMg+A7 zlFJN3uqilv-vk$#&gRD0R3>R#ZES38k78@wVE`HE2SzOPOT{^?_Bd{Jc2UCjd_8c|Nn!d4+*i>|p`){+p{F-Jh|Hnp z-qqNcI8%eNFdavM|9ztXH#sGl|ewQrao^41y17PML|>H6(AT8H+JhphZJe9(3%1~C_0anRC>95GLsiX zU5i8KusCk|3(wBdGQlX$e+qJOpOu|-mcdHw74LN_p}#qJKgcF5bLAMBL(0bHvd!MV zUc|#!SzLVKV+60Tc$#N2lF7MQ`f0}RLoHQX${pg8#pPl}kXp5h;W1aMJOMO|Lsz+qSny6o{mV^I|x?M?zURW%qKKrt}4jb4W;NIuR}tuYa4q?r}#%jLR{% zbFsnwc6d8s(3tw$6Fz13jPn&R0{g9dR9sw~#5UI~ z`b8X2G3_OuIyuJJMTR(qHvMyZ5(yi3JadD=4XD!X7vW~pg?QmGxX`6%=w*hf7<6Yo z0RJ()Ds9C~Xeu(se`Y|3E;aTkCWraYhi9{D?ZU9=-7h6i1XkCFyCU+#+lDm=O{eZs zLP!|_hfV%VKAF^GPXKJmD3a2_!TG>}(lRLFG~~vyze;cZQOWL1Vdu6iW5t@Ec+MA^ z?owSd>5S7)m~HM@i~^MW*&S-y74Jt@Z3sFQXW99HYu`IYZPjhU)#3E2c@L^U^HQGBT#{Uc&F@hK*)Tot>VbHuv3Y-<{fS z9bVK-L~+-xrn&1Lrn!cbjrNpODRf#d3wlrVaLF-22mg2i+{@zQ%++6}E)!iM^+ibDgg=X)+_pDqk@I}m3y?a5tB zeUSO4*K^eC+XZj)(Iwm_8UfGO`j)!n^jdq;Rw#Py;S`+w1fFV2HJGK*=$^j&_PCSN za!=g&VU%N%yV^fSg&Bb^-tIa~v2G%$Sv?WJ@NGOZ`8L}})Xs5FN>3={z4Y^Z{qR1I z8xLfn`VS=E_G*ez4Pq0a#q}8sUo9I<4&ro6LP*)I^>Q4+SFLQ(32yxKt-;uxKR%kx z&b%t|?S91(=h(SheoiRLM}X^YHvz1?!?k`Txm>@ApCEoc(c%R`1_a^YKb0BBo8uR% zCb{5p%3_$5&UR%fuc>(_#Of+FIM7OAT&Z4lJNckmV5xdFL5?G)B&SVkG@1NFDoIq{!Dd`ivw~e0e;*RQ4)AfmfPw_r*txfHI+pXJD z(32L=>Edr%>ogCbxJG>v-u5s$Um^6$%RFg$_JSuo2X}dUdp!SK@f^vC}Z7*5zR1S)+m}(lcX-ed)m%O*-*i0$uq|MZ--J~3Jh4QKBaVgx5}X8984->3<>h0`RZLE9xpt^pj!lM7&t*&m^6cjsH0ozkkPHRl z1zrG9`q>vaBitq)B08(*;#TG`c_~Br`XPziV+g!!&hC1?o`Fx;(PFdCmfnn@#PLfz z>h&8GSD8L1=gh3pXvNkb#YY5A3Xcp?SZ~k@=b&>BOA{LoY8jatWY;58$YviDLu^Z& z8F7x}2YaF0S})rRjDLqS;uJ1nd%F)5WS2|XROlYams?2ICRxYC+ogEO4(JH4Zj-NC z_8khi$A%Jt_GYT~WJWSle_x79?jbr@Ne*I%eun>IWlDPLyvHcp*i>?z`*Uhi+)2^i zh4J_{T7keR19G$psC!C+;ylH=#piPb+lU7vaGh;qVTi#{+NGEAsyrgYYfy(3zKVo!w_P@HO8D5fEJET1JLW*Eap*=#1;KbsA&4d!9^q zQ|~>hR+s&ei`_TytXe-Zqsq`me?wq!`SbW`gPZQ-6GB%{xtQFF0*Ir*2`pR8rt$DJ zbvoJHa6vNj@+)({?jJ4Q9-|}OdlT$I2<9yXtQO+=mi(HIld09_j<*Qu^ZkxmY6qpnrrEVqB_tu^R|(J@v-~x$D(h(&QX{){q8@G3WJ(M_ zB=DtC4i39jl+>Xii$XhY@248xMSh+2w5e2tf6e$Gvr6OXBhQ(l{m zoMYL26IYWpXWpVH0z|>j&%Z6N{IlNm@I9U)9>M#O))^jNkDAW#Afk+Ri82-uSVYf} zUR3Y+FF=1%>vn;Ik>8+Fh&NnQ?utiC^ag|oongxe;sTGQQHM}v}&A7A@q^Wby4RH92{SlB2sjW6?YD zT0EcY{9KL+b2tvlZ_d2DZjHCk$)EOOxTl5~M4q7TYeY^;c4V1MYChlg(ii@a$F!Z?8=1Lbxq|I$ z=hpYC(YgGJU7z2A4T2otbl3wU2ipM~xuHVUZ>*2Y3ueu@&4=)!0iK5*0ekBl`}*Aq zkctXDyM}s6ewu1L02pK0;h`ZFk*vI~HI!!kU`BR#UoceTQt>^<^hFtq`xMooru{Qs zWG#;B?bww5hS{2xoFZ{Mi_)LG1)Jl?Fv|*?^u! z423FDIGMil$~iVK1<=OPhgeEI(XkQS+Z(92`5eav@nq;2xvg zo>t1tuf@2KTxeH(-Q78_5ux3QtzD7wQ5qz>FrK{gDAv8{$UmEVc#YYu%uMI_v`E9&CS+qVL0G6csg2s+H7@DC ztp-SYk-cODXGInGNo67X*bSRa9$r*Ynem*KPv@!;e_VDhhe=Oxm{_vsYnGzJ-4`D;U|Ce?WH&BiqREVlspi z^O~gO@0Gau7%hE!7sy#PI6J$}&lZZ`K0CH-G29EYp%;zCWa{&}#9iZ#VJInjCY$^T ziA+v!+LxO|iJpUN2a3R892l%p+RY&tsL{fZF}nL?SUzhW#;nZwr9J^*6ed7%|CzfD zAZxVm>dbA)KF#aM@rgBpXkmIko-^jAopw>s1Q_>Ox#dc)$l7IGNIVl?Xf&FR748ee zp_s_62ebLlhSU+Gmy^-Oppu=748xc#Re;5+5nwntJ3j{Hc5|^>snoacu0MfbP0*tu zjC7XoN>TR;9=d;xpPIO zmHT4(6@bfox;zo!T76yF+4FvdTc(p7UsrGie*2^lpQafNxd!xgba@P;ySd`e29yH) zP?gr8Np}SH(OX=6m`Cu|?$3k>8a(eE92|V;Sbz}C{qcA5hj7^wCGXkyPQ6+c^)RD+ z@b65o*o|z!s4pe3$9JI_Shr0-{HO;UVZ$b&UoK{BAc`FqZe(=}=B(t8>Dif?nV|XJ z+mh>jXaIi8RRLOximS0UT&A3|kn55?1FH(*@;+6l;4Oo~Ex}eM*sm!t4!X|aCXx%_ zLeXu!J8?av7dz~B7QutOg>1dbKXXAq%Dip8! z5}EY7C?KESbpdFKF$;I12^Y zf)cY_?-aey2J=~>Es8=>gawImNqBxJbbgG&(cxD>&u2+^pe@nTP<9#)JbAB+_L6Ry zO_1Oi89$;B!x(U)_?u^$EY9`xE{4cgt8rwxEm%Ft7Juni3)YT=VP7=a z**WpjoOWQto9WO#PPVOvw|t96z$m?x-d({ zzK@+F;6|J7>&x7`a*uJ&yLcz|??N8es+0E~^y?lq?koi=RGhb_8g#4;Iyc)t%8P+p zp4Z)ZF~Kyf#}5FlzLp%?lB75%Hm)QaNNrUbT|Y`v>)P<0)TE@BH!g(QT8{2_5m$Cc zS0H;Sem3^O(2>|mJg*@%wLa2R?Sk_Fj|lj@c=bg@O+B&60C{;6&vFJ?ts9XWYb!{Q zliHoZ8rIBqe(t2|(~;-YiX7wd6xuq+!#{pM7eP?5PfskA{j@bUaXPTrB#YhcLUgbJ zEXcDG-e!{!Zb#4bJ1)^cJ}x`~G52#l1(0~^QFgCWrk5a{X5L@vdv*f@fdtt#V$RNQ zs?7z9Ta@!nZ1XkRqvW)b-J=Lw#H`5dRp%CXQ9E`Xo>ty0kKQZ-&(XHlmQ=`Gq;lI& z(Cd9DSKi_VWU%j*TfX0)6GU2ei6FgWrNRI~Bs zx)<8_XS)|_`eQd(9Q+239?R;$5E2JzdZAn?or~$?j6b(A9Hth-hr_kzj4wJmy6fdP zSa?_0&VVRf-tMn=CfT=(=yaWF!k!%klbA7nK%KDdbpRMr z@NMErxbBbNoW&f469YL-6q%n-u4tFo&CKYu4;C| z36@ZaNSO_q=6Q>w?lEonGl>p=IoD&2#oWR&5J&Hw0r;cx9pqsgtPaM5`SA5xFKe=C z`}*kuem359+uK$DrfAaFJjT=3w#F2NupO=#Ey9s1!;{Dd2jI~~_?~Ni9cLp@6w0Um z$ac^KxP99?wTHm;Xf(|MZs%-|OHBq&WSWfYxQ#vc*xw5-ocHD+3=o7M4ijzV4x8>W zx^mCp+N5YeoN-lS1^>-9yvMi-Qp-p~3?702{{+FeAxt8rIqeqa+>!N8!AgUl<4c|< z`f+>(=qJw9^E*G@o>LOuUWJy&zy+7P@$sS~ce~bor4kaOa1Iw8jxwf(eb{&W23;xk)S%PtT>)`MM#D#~x{kBng%h?+qXyP644_M7WPH9cHpLFOh_3i<+BzgOg@Dsol@Y%XmE zZ$M1%1)Do2Lvly?CV0v&udt-QSgl}9zhIcOg>-@gLin$@c^|)Apq%gXef#(5=YhPv z`~3M-&`uvuQ2PU(a@B8UPrU=3w{j@H_=&Ngb)D|TQ=Xl@vV{DFU-AmX1+zk106Mmk zXB{0~(csmc6W9|iFx*Rs$f1q#RGFn)gc+ySkNA_fW&IPlSfNRAY*Yl4zGYac6D*U| zaa{i!MBxiTMLEyEBYrmriSl>A@LLt=CT&>ti>=O7nonC-JLmZijmYr?UolVhiRX@_ z%MAI_N8I%8M|AQSJa=DI6luB8zP_JGU+>II-;fbGUYmC3ZUdFlwJ?BA-c`4mN!UDB zPoa<7b=yu_eeHbPWLx2e#@+7CWFKkmBiH8Y_Q7iNRij()(=Qk48{Qe@ZO3?l_9#tp ztrM23WP}Zrxi0(&M1t$LO&8?e7cBr9z#; z?IO4k-aqVo;~;z2_aTnrbU1w8CS5JrrIq9fQkuzaUA!P2(Fl;oJdn=nQQyK|q+#Cz+J_v>c-QI3^L9o9q zZbeL;#RWoDfvmv1^?28?YPsEEZ{Z{Y_0ZXb2IWk1(3o4H4&dAJIIkw;JTt_K$IN2W zNy-IRnUp(PcWg^?HnB_)B~sz2OP7sMjH?-lhZ&4GJ+-MF!^=%&2eIw`bWrztM*&_e z0aU|mZQ>;}Skt$80b98VJgvW9b;^JFhgr)@!=qJ>K%G<>>e|qQtkik$+bY1I3!7zom>jx4kkY9>Xu|~+tge3RRM~la zSkaou)AQQJX}j>P37*XLA`ioa;dwqAzTwy?AP({zE+o`5sKImzIZ2STu(YQbA^p+# zxjS!M8QdnhNj01ynp`*&d)=PHO>slS{<2b$l)PS^PyTEzmEiAes)TWpx#bPIQSyLT zZ$=szsx7YvrtNhD z(%)p29{Jpe>sOSiUkT_}Uj*gE!`r>;xXAbLNhk97g5aloQM>f+@0v}(> z5vMLUwb;7LTASbx*xnUBuL3kO_{AFWq6HibPR(kS_9kJ{x367e5YdUa@!zjI2Q+*4 zh#2LsdHhvabURMEDj%m6l{I_Rk`kHgHvGzvRZp-}dEPhMR2srF6@+xuqNlT0HMTAF zgnr{DH$PW1a~9K>Q6FHAC%}Z!ZAp$_^zhIIR%t0Qlv!HNJ7g-O_@@kaOT%Hiy8utc z>YA0-2T|OrLfoPMLVak1cF=o-@$(+jS9FfHi=A-EPEF4_Q(7xii^kk z@}v5hVXHl@VYx9IB#&MoMkF0is>79?9%Fq*x+7vewO!GUP3N-GeJ~)N}#>XPATVBgU9j;V0gWmTp$m-`i3(*Gz zSJ7G*Es~PG@mrvAwF;|;$oE#qG=?60sVr6@BVy0Z z@nB>MkT$Yz9i&_SN_4|rx#HRg zoOGjLM|9$+EZtVk(3m4IPWNw1sI#Th(Z>Z>jQNKGhalOX@&SAwqTf*OyE8NHC;6ct zrH1Cim13tZ*el^Go1{T<;- z9C?eQwR$1USN0KpONjFLgyLkggX%Bk!Xn8#wjb=iw*XnW;`mdSMjy5F5U*l|6+eSZ z!cgZw^i$x_0>Y?(1xt#j4vvls4ZHPRX4AiX>9!D?``s_Y|0v zXYpYc=mmSvYdT^1$(J|5hrm}E5X{Ted|rzv*6J78#ISN+&uF z$OWP0{*^dHdLr+8;|-GR55Z+xOF}Uvp!dGm^ScazW#)W6EBu#y*fW@DW}4Y*kq#8a ztP1Je*@$Isa|&E|tqBw9);J%ij@1p4*F^tt@Ldy8pj&$UOXJdAA=nJ`ZBAa#9so-f z?O6z=wW5!KT6SXOx(GMHe9>5r1A~|E1n%G?IipUIiL`JoD_)nv&i^`ttKar2o~2f9 z3ix=x&~qWK6PG5~iSU&DTf(oVc>==d8j2=mIB!qG3l@B{)PXLyU!eEb>JL4XJjw~f zK9j{HTDY!}9#Z-NiZ5Fl6m&nE{tAC+58px+lgYYPS-n@8bTyz3sqJI-^NIC7uH%0H z;`yPJ)(79>YjYIbX78eIrGuwsY4+=C7$93z_nvoZ4*bb;l(@1Ak;n1_QBg2J{UPnA z-YIwaVGTfghdtS;Zl**Nazh9xR(;h_xJCfm=v#UguI>()<(fk;OPZH6c#(?_^$|SDj(}w3HL)ffNehL#90 zTBa*Oah9zhcogGE>gD4%!i1 z1x$ps)DQb0x^itVE1!sxHZJ2Dx`6*s^@h%J!FSpYODZg%hS+$<`h~jbW8|=ru?dzM zsMD}V2cN4C*1e>?Sk!P8R6ZaZu7@}qmsL1xONO|;6#WmZimIF@kyalDjffQq=K57S=Zx~ z<(T-K$9EfF1f^OIyo7rljwMF-HKcZM#kmxVJo$2s(U|*uUR6)JTISER-SH&k$o+7b{s544>Kl;L=>!+$+wBN?}a}4sKTHEL(c7hF%CC9Ugcu zT?DWC-UvG;2AV;+3t;ZHPPsl#qtF0xadozeeoXXP6c}SHMo=DCAD9u2i_$fllXi`f*}l?TKb}-HpXXxI;Mj9>@)rhHZvB+$#fOoU0|wP%{u6`7rrdb8f&% z+?7*OODAzCn?wHv#h>F1gvn|Z1FffBL>Du$?kMl=14Go#SXE)2$ZdthsiySc&-(7R zUwKY{R}7?HZ?rR$wcVUc1Zjt_96UZnYvl!m-EB?ub@Kih@*9G46S5VwcNd3~3yu&I z4>KcaStzCgnU0)a5jl1m8XBS%3wNy=pG4ZB9yrA|C1LgGfa4>Elx6pYI*jtrnjgJZ z%z0FFDxXm(thwlte64FN@W0-fz}6KBnr{bKjLjrXiIs7ZQfIM(Wn9q=13P(*?g67fQ-oeIF6<9@3^@mI zK9!Z1*Au;{f((3nziyZL^AS$2?6ujcHiu;A6Q66SD*-%7|L&{eA&0CzJ=T|T!Ez6m z@%Nj`IU|!`b|>7N2LV|zn0$H|_7E2xOjc3e;-(xzqmYmGJspOm(XPpxb}QYR-uu+a z&h}@Z*(?Hhl+^QXF#7OksGo;W!VF)bpTy2j`~6&20Xz=7*E{4MdZ3fT{L`1_qaK03tU6n*bbL|^3`B{Y+W zE`LS`O>MtV1UdsVumh(Uh>sra5kGkeOgA8^MzzkBg@?v((@Mwq01BRneeCmPKbBDT zE{d#B`Uc+utQb zRo?GU;8Ca@;cysl8XxaSgr2HbY5KhjFPT%**$bY;^EElI1xvw#8F!G3Tkww1PG8XXZcv;xS zXRrr{q6_azPxxx|UjRDq#4C^pcv+}6b%nHBhvqd5KztAK2+L_SeT;_W5^Z*)E~fY~o-#BpJ6Uq%_|phx-b-tg?jdiN=T>V#KdaqXw#4A) zQ(#x@#DKESt@@0>AAnw}ZETKnvIlP&%{%I6tP-Q0b{S~6BM50CuQpGRN-M7d4UECo zckM-V?4B`Z;`s*q39mK@0k;niG~UTTcBmu#9%a802b=dEp^m&i=$LmK4Xo#>V<)*m;zAqM+jz9~um`z=a4;f|8)li% zym^vFxb*+g!#qtznT~Y_`4qJqVJ?*(GiuhP{rmof1jwG4+{mMh@YLITbfF=Yaq_0gjJtbbKyrJE+P1?V?(^(U}ty0J=I_v;?=D@{h zGX;to-Kt2%eJa59AU~$R3!utI(RM3P`!#nOn*Nl4XbM=S^3OzHk(?&5NzPwP=1ina z9QgFw9cAXom~tj^GN+RqPu}w~lRF({G5_6|alN^%hGMaD4W)EC%mln6;g*D6+c{Vm zQ^69TV(;v%WqsoVtCXclUdxc_V@id98YSNe7TeZu>;l% zaq*Foh=>T;99iR185O(cAAn>-=`ztjHe-oovUjQvK;VB+fW#5VgEw=;r$+sdZOjWh_bi-rL9>89PoEb9eNuL z45T5zh-U*swU4ws0dzJ$c+Q)bx5f7d(-nBLp#gtq?FIDz4FldLKQ#vuNfcn zAZ+kEaIoe*9W6~SI8Zn`HCCwoAO(K70Z?AChp~0?WuKOso#uveSdXOX?V5d^@DE%r zr!-rdj_a%H<$RiGCq~&Vb@dU{{YoV_Z;pDF~zx;mw>L&p6D#%0n!AHnU zf!I%UZB_hSyovV6VxisftofoJxez0TaV-g&Tv$~lQ?XvSAeH{MqLLTuBJ`_2(G3)I z?PB#fQi)2#PZLq<4%7>Xt*t}-r%3wN-$|~yg>qr`5_Xq|nWhgJJ5tBiqFH%PO4EjC zuCcOZ_?b&?E-oojS)LiL9ZhGc-yz5gT8Pd8kv}9(wHC^YKzO&H42oL!;Xc;BGVbzruAP^4FdAV%W{{q!#7X-e4G9Cbbd{A4(5?e# zZ{N`WFud0;CXs>g+shxTfvnkocOT+6l4r{Zf$6Lb@b)4>1r=SY- zXwlEv_%FOEDJkKt>%$@F<@eruFa7%0zvj_vx7~Id=b!+W(gatUzus1I2u;**O$f}v z;)G-26^+BB)M0fHYO*=|1E>VaChD>GGCe;0>V0RJS};EIeB404A(18)OsA|={=a(; z9z1xDr=t(d`6wvXqfSp1=tCtS5DwvF=)wyZMi%@f^wA&v=toh%aByP)b*Pqn{9f1) zL^K^1CUHzbdM2K`f*e0P#Z1+V>(4643V)&M?52{O)B)CfDJ@vAK)g+CZf@oc06m0e zb-z%jt_t)}^g}kDz?-&|6uLZs*g913q0k_LVW5wmJ$sgvEOjObfh#zNlI}W42k^%W zSDe++OBqR~puG>0jRr1)4+ZEwcKI>+@EnLLp^VG_oN@b~dp>ISaQi-WTB|^ZBH2iu zVhj&-cacTR-mXr1q^_10Fi`%_`~?x82;6YXmMvU-udS`+@#?Lut=^t{N)e23LlxTt z5f%+BPU3nSJ1mr*6x>*6&}pfmeP|MOqFzU?A*@sIU`Wz#)9dT&dnFy-zstt6D$vIc z-N?pEc=IoZowhKYG#DLhZ>LMQZ=)-6bLoMZvm!b%Kn@x|ems9w8ss1t(!k-A4#0s3 z4}BfiT*E~2RFg5J_}^uj%{4aK!ZwgzQ2ba|H<$3YtPiX#GGK`y3r1R(aO;g zKfZBj+w{r^>;mIm|mZ#*A6xyd~Uw(yJWN40zXTp!Wj+c7m#oK=M9u! zPiO=`6`q&ru8moRj}xa9@SPk%~%H{C?~_uixHj~=0Bg+UH7qTbpE|Dyeu zUlA8K1K^I9*e)e=QZgC!9kRL&rzRR1*t`7#JrFf@StvgvWyrq+QjHYkUs(DT%FOoP zT)of97ALK40LVeF{PDH(dVXQEBNzvPG~m9$o{cYu zMhGiJ>p&8LQXJjMuIZgmYw3cbf#J3O_6Nsr?y}Ms=M)dJdO_FsO}Npq>-)pDqjcfq zyg@$nk_(5sDDK_3`H}+L`t81?jo9{z_P&vAMLPb-4_Yj}~gNov-Bon!l zb;_-AxPuSRo=q!q#zk}-80UOBvWQ6YO_Y%Wx4ODIek)z;{n&NVShG+ zcOkIZ>G5ZONgdtop*WQ1bHvjVlJ{B#TPga{ekb9*W zcHCoS$H|~IQF}`#lh7ugr1S2(O~Vbc;R50FnE(#+xM&A1p0oGb*rL}*HD_z+#g|{;>ekaw{ht2!h3Dz-Z@fm8M?P7j*Cq(i zV|C0Qt)e?ZOzYhXpFS!v-b1COTR-4>wqn-86+^Fu=udPH1bhGuuX1a(Dc*XR_r6e8230E_wa`lpcq3u(4%wa z(8Fcn)hq17!~w|IfEC)v9(D=?g*F68H*DCzv*2Ke!vtBgX3Y>etA<Ob3_NsXvEYbpfD1#*=6+Q}3#CrT^>W^>t}be5XyBp1Qn4EmcN22bWM2>NfgH63 zl82o6_qS&@``yo!lt|mz0)Wxae*f(BarE$OPtvDH%ISf153_49aqxQ=AB&AMZ_9~& zq|xdI7<2TwCA9d*&rrjTEp+PXM@er^x?Owgf89MAp?Idt#^o}x+elMW)7dL~sNPT547If~#jcL1vzpCLHhSvZ+kF=I zz?0Kxv0S@fv2C5n?m*69&UWSueg5;Gr#IhxlXJn#F1swONUwMx02kb@U*aqS*Z^Q~ zf9|$oze#ExH!xt-w)N1YEO$>ga8=kHM1e{e64Cq-$7t=g(xlvUUSLuVL4}c%&o42M z1*8+7lu}A+O6Yr_VfUcdMGs#61HL_7n?l!o??zg7)ur_R?s||;ln<=I=dyNF{w=?v zlqFZvk^66@*0O49xY^y4-OlD6uHN}4Z)UAQGktzr!WGwxZG7-RJc;i85LbTn#D+mP z_?4ERY+gI=vR!bO^K@LE5OGE>-sNafv6Jy6C$3 z#RKx!rPgoGnT-Z|e)lQ*%#0$xdFcK`cW`P*5TQtpRsP+LZ_>waZR6JVU}W!gkV$VG zu-JL;0Tba!-vS$aKc=9KtO-u@C!{9|hgGo_$@~qo*Cc<~btuancqky;| zjW#Byf#|+x&mPXjo_p>&8fnyJ>!oC4ctRkiagU>q$1?;XaAk03aQkr5Q{^sna^|6# zEh2Jw9$(Bf19!AptaRyrtfEKWd5Y$IZ5loOlsii?>%vQE`JZ>v(&x93Xl7{wBdm|SO8793&KTLe~$kC$G=drIr#I?Go8D~ zW~J2RRJ!;1N66aSNw2^9TdK0`q38az$Mbv!!)oqh8@=zFcfQB;^IuS(-b9Ac0CW@6 z!yfz1({$fGx5qEgcR%3Pu@wP&Z$K45h>rK&cc1rvL($LSm+p3H>6H%-atrlcUsyy3 zD$X#`HZ|&T?oC~GGU~#UejuJg3b6ILf^zj%i-T9K!NTQF9QXIzaR2-Tzzi}T$#ARb z^g0?}Qb?W6?S9W?x7z5E>#m?Lf9D3i{rYRq(4SxZZ%>XvcyP`B%;za(>7{(3>Sxl% zp?hy4M{6^ihT4${bkNQZ_tGujUPIYgacFg~`GCn!3Wmg{mdMh5!I3dR!z~QiW$&X8 z%Ij$&(^o$;HJ<~0`=_V5HT<1V&(g%KWSW(i7L{8Crr6wVqvDM4B@onkZ)4IxR*UgR%HIbv#+5a+)7KYGv7B9hD}H+|7-Q6pehzlS zu35c`-g^BxYP0F-$v?h1d;st5CVJ~jZjdWU@Tsr-L-aI^HVfsaB?c|8>@|S0eHIsI zaW`0bLkHK30jP}hw1}$1a$Ph*R)20`mqJRE-ADynQr}Zzq%`LA6gPy|I7PJM`|Q`U)m*1=0thVaSH3DstlPJ5AMbItdGqE$rKIqX!9{$z zBI>SfJzh`qnb<20{0QYoFU~MiZ=aJ&8@hOk=RPKO+u1fU$&gSJio0XNNf!kRlMa%M zI-buR==TL)B;4V%IHr&?@LSIx+ikG(b%SwZ75g&c)X(&GQCd_ag!xNOCGt#SX?cku6!zt2IlIWY) zU#9T#*i5wbHa5eexR5|EU->L!72&6+`qM4_1kL_aJS!|3a$S4__)bR$zfL)eh6i5}>*$IB9S`H9Lp2d& z2OLAE)6(L(Q)%w3iDWX$uuS(daqRS4L?{2^p4G7kfN|Q1zW4^4h01wCeSr>9`Nh4J zG;jQ116a6`ank+!J>dGv+`@oCBECT1ccz60zZi81LjpPsa%Kkd3kuQ)03BEK-m!YV zkOgzU%^IIg+36{=ekZuET-PNQ2UxZA+u)A*t# zjt|wOsnbdYslja!0HhqZ8}1)K9;)Ju6o>#ht_At(k&5PWRf1SkT-SANA?5b6(XxXx5R9;>_aK+W>ypse2C6fP9T1!P4$zBV!l4E?DnR8Nu ze&Bii4cJjSZM|I29_slZY1tT8*D+v8j}7g?WYBs+8fXBJ;pE)|fNPIdwIk{M!2*N$3M^=HqWMMzM>m-al+f1}J8?Xq{%;}r!YdJj9QoI< zc0c{})AaDe57Vw)y8G(OEY0oJ@Ld7ysQ?) zv$C=>y8r(By`KZ%9~`crou!pwRZfkZ%My2)!BA%K<2r+wjXZR4A|<%$k*a$7+VYva z;%kyoPuq{48a696&MQDJbYyJDx9WnB8btfKljg<;s=x`s=UrE-j)F;Q5I+JQECVv%ejwp*4$3 z28q!vJ$A}XH7RBk1c`3yltY?Kfs7a_$d zt|!m-?b~_pHHhv94<4ioFT5~rt}Q#+NPDYWxMdt7ya<5lVj^||QOs-x@lF6|xOD`i z3n!uh<)qb)8lZ>1D_VCU2N}g2Mhb1)wvAS-SV50J{y5!z_uXN&5{d<(=;N{GE}D{; zMX&8YMcEAWH-B~x?LJXM8WrQJj;@kCdg!5t`0Ks*x{C+K6Ue=~snbd0GgIhuvnSBp z;v70$S+9az1-eS3jbQN?#qxTKWO4Q|W4)ujzoXYN;u>D4Kp&$7kb~B)T|3Ymfr1=t0&5cF=Tb)-=%8?2 zU(wv(pf{)>kAM=?TcDCkDo0l-281w7@LB`Q zuzxtt&2abc?&>6Me_z}->r{cRQb@w&<$crB(?@0arAjK$Rg&oGDygLE=qjnCk_vQ{ zR8mO=x=JdkK#v9?Lnjyo3z!HMOo0rbU=++!9L-~^q|%rJdZ-pT!J5;D0%NFJUBSNZ z3{m|i)Uj2dtE2*5C6!c)OH$0ehB#y?M-{^#2ZSpLpNSSa{y1_d(LU7Yf!tN$9$SQI zir?bM-LS`s?zXMTd#{wTY9Oi+d-$;Ymd`K7H2=5 zb2#ZQ3v$WwNCA;4jWp&=J`%<+CT-y&N+`LQZ>yvNT?Kla&@l$$O?BNI&?c?C`ZGGO zw1iewR_gDp<-^r*l=}8R@8=tAKSvK~(kGDN%Ew4M?F#kAk0qd+OeXbi#+V*E-A12w zImqPGvn^2@O;v{-+mKBCmKL(VaqIbiGw^GteUXf7UQ{25}k1pEz1hd2k``q{Y zzWa*pii!&E?wU4jTEJs#f*d!j>%#y2d>%{t!LBwNJyqR7W(kl@32|b-FeH()Y6n>! zo!}Ai9F+KjgQQ8#8*`w7Vfx&;bCjQ-A2;?T#egpLd&@DR1^e9h`@Z{%ZQ;-MJx-uw zM|52pwtH~Y8((kI;=?D|cH6sb!futmRr<>E;yBOfh8JEu;(Z8pw ztBXr@qUAL}jS+|tpyjV6Gg@-Rs^OwyK$rT_l?s6sx#0*)mF_3q#w)hHqPkBAtX`+( zV8PAB^$axxwy)Pg4nHBVuuUi#Dfif^Hmb1n4pKWt1sPKqmtRW#j&3rqJ4%|gf)R1q zi01$x(c&0{E<2Hszn07ZeyC{DxB$Aem8?=ButM+m2PR?s17Lj(fzb$o)#(j2VZS#@FD_9Dft6afBN?HN zC+B3jN#tn&R){w%%sHcY^Cm=bJW~= zig8w^kJf&|q$C$j7{53wVDIee=9casUb&dImR3`HkDWd&tE4`Sjt*Bh((J-)y8MC? zWz>>5%g;~@=z%iKDl03wac%01n(E)Td1XhOBE}KGLKYQ{o-F z$YN`!rgO(BLF4Ayp*SAEURkr3CKg?&*ySLo#b1Wy9ak)*KBlk#?%n;gVpcJ+M?`vZ z(r0EAbFc#VO`R4hJKId3W&c||$sOqj9UonA;Y$}@9AU%*V-q$jEH2o_`SCw|2iA1_ zPq=xp4O@Zm2#LCdk-G7s0`yQJuz?I)BPj*d-|q_N`oH^W^XLY-t&l$SF-;>!Z4f?2V33{-c2aS0u(YoT2gbu_1F9FMsWi-WeW zm*(cDQF})R?Ks*)ZMI&jYU!aR#hEluuVw1TK-mM_8-VTU=^0d~L3$mSJNO)GZ+2z| z+1PuF#mWJX&q6gp85W?23V{t|FhgMptXz3{@gEZ%i81cqEmj(J7>D63F_RL%zmKd= zJ2f_!yLD?BTwB(809ENlNGlCI&wJ6_1eq;ybC4iQ`sV2Wc9Q zctvid7uLpeCn$g1%!s70aDCEiE}BJEr%%z}PP8*lo;o1N3gR}vFBUNu1MavK6Ca%n zw1>~O(eb8EviCXZOA98`t`jws#TEsVUdwF`{hs6~fW9~#0bD6`s;-$*6AiR_P9Zti zf)WiZ#Uun)h#}H2RjiN@sqh{*Pr3qz8v+ZpLHI?bA+WWzwUm{f<~IaZh)ljQ%HdXl zl@kKn+RG-#NHxDPu1z{KneW<1cAcK;8;<&L?dXR2*c_b^eIGipZ`7F_2_P{knZ9*l zCUscs^pCPSnpc>~K`jNLl*@bPE}Jitvy(Yss~TD;Kh-_ITyEc?SCzp0mP)~WP8Vr` z6E>HF#mx^l&~w-gsNcVTzYafq?%cWF5Lf^Y0QU`t6|1B$Mo2#oHw4z#$RZ7a)!`g8 zI0P2@6DEN_kxuC0EWBTH*ACms^pHOp41@%~4Nz-y7CcyS*m~%S8<%X90 zF%)ZeV`C$Y8#j*Y=xOFe`o@xpw5O_t+8OBc)6RoX%H@5tDYZUxL|WN6Sa1&J9z_49 zrY7;Prxz_+)DbU0KZoBfRyhXvjqL5(s;Vk}qsMxA zV*ZQZF)$$^od%7(W4C&4nu{t8@6$8HpoOjl& zS>E1j(!vtberLXY+kAVa`SwTiwS-H1ubn@K|DJHcj_t2#h421y7O_-a{4R9v3|I{{ z-1ItWJ8Ov8h&NBjP>oX3Xw7iBIaD}EjnZ@XGVyxN;)zTQ@27Kiw|?I5?4xsCmI3od zxXy$$D^yA8ebu@gWO8_erZzL(?(5OKhp`CoJwu+DdWhl&4<3Azy}bt8Q>M7X4~=>q zz4&1%rJId(_2Ow>KccuXzWvaAw|y<)(%x(5b@_ok;iSj*1+G8vO3x9lgpwG?`Q(VR z|IedM^v4U9QdRANxO8_K^x^BQ$QcjU4-IZEa?Hg%5;i1Z+Y#KkFf)a=mY$&{6UT9& zOXm#$pPXc-1h${qpr^LZ9X|CskIT zp-Dyuby^)X8tHRciE({>;8`!$S|~kreB51|s~_MBpEJvq0svkt3W#b$`6tl|Ov;gk ziw#CHNC{~3_56-cDbu8*lXWeFBtq%j5Og zj-+h+h2qWW)2COlmn}l{iUuJxSLb6Xjg+p>OQyG*dgA&fclNnxTbqmKC1#MtI-F&m z`l&E)PGn*~dwv)Y$K_Uyfp1*YG8B3~#P_pj&x&2jnIJ!Fq%oS2T2|v-q1$8CW?UYI zjmg-C`)(+1l~~Z~+$8!(a}UKiW2cvEyJ*GS$<$-rN19=Z=rfx#NT*XKBT|4IhF_^w zJs4LnEiIKw127=}btJ2fqosb1w@gS`S=k!)vPplLmDXOW z=*^%ky_tuhMw<}NUYHj>Lm(FEg9p8a@OCzG*ccPtLC0DU`g)IUuW4;bB*zx-uvIDTPc6sEFQQLP0C0sj0(u%W)shtf&4x?Zz0bZ zD@^kWGBL=;op>{G;>4)y_YaYEO9gs-5;8D2 z+kt<6_UzeFImuI19kjQ@Hf%lHuFg%SW%?%SwwxQxx%-`zlQD^slXAj7E)tlK$mAhJ zqFp=ICzJ~GkwJ*ZM_fMdR3j(+awzoN*-l8W#-XL{y=FFK)Gnd_s(A zis0Z3DPqgDWWPwLg_@2*_!Z8r|4|B>L#b zOoiYNA$1lK0-1CauBC<&EicGhB#ACDvY#!Ame=AR=uV3mdQ`4OJUy1*s;jH<(btuU z;9_~pB+(^C7EtvDMdxZ}&Ya1mILcm$USwjMFku4!OlcyBz#9a^OqREA^g-HaqYDG= zsPx3eAA0DaB6c02wpe%D$F`|7idqs$qDzbh)KIvdTG9n85i<|zt`uRMtVs7UQ-o2f z?h@IKTE!MOBgrMzs3BtpUSM2tXV#iBJi7qh=Kzk`HnbGv=)2Xzy)X;;&wnrmM zrG7d*^8rP3Xp^^I5?vzwK(Wb{Lfu9~2|!g~F)C0tS!q+G31}+~qGKrzt)zGD+J&*w zO5s48x$-ecqD!O)C>Y<&HaiH2w)Nm);ZowOLcpW!xTvl9^C|}u{N2b+k3r4uNaS0W~;kkCRTq(qk(inoQmi={0@w#6eS zKGQ;8gu2$&)^cYYY;0^=yze+RCYxBmN1c5h&TAYmWX+*aNe%hh_fxJVbf;o=pq~3$ z`Di84CBjZEUh-+;#EDc@Ri*90S*@CD!eKuU7ujodb#=Umr0}RG`d-}6d9ODUpF~jC z029N0%BK@&O`^Tqw{SC_WO*v7x{q;)WvY9zdqoadbTU&W26%ezcq5|>kDIL4V<)z|0$B?X-JiCqR+k5$YOe!Eevg9Zn=!4y_ zkV8NceY69iq$u`KF49M}sL;s*{l;xJ8&|2-_BP0ln0AzQP8R#S`=}ttKIHFqtA$#6 zT!a3Gym$Oom<|*U-38%i%Hbew^wEa2Hd=@VM7ALkE|v<8EVs6{U=96Bx zH|lrh&Yi1n3V~!C*%U%b^ictk?)f;>W`vM}jur3t0k(BIJ?uvYhaA%%=qIb$ zO#Muwh%&=8^dV07cOx-yJQci4o zRP%WIV{IGDV`|ks+Hx5t9kgH)S#WBJ9?yT^WrXI(Gld2;1nvu9@ADj13Nfj%Wd?T$F0?7&+T+0$12nWK# zw1}tb51D9U1l{0_1vw7t^Z2<}UMo;ALn&t^idxE6%u;pFDxj$$FPkjv&*AQ2?sB<= zhg%b{4A$7#$hCw436Nklz+tseFvdKR@32w3)2;iuMLO)Z@ao(^nQA%2B+-W(SPRFR zxD_$!usO2lpE zm4~wo-*-BrXPvsdejXSTTiTl$(LKqV9_(5$hXe~Nc)|>s_-{dE9oFqi;51-f_w(;)8(c-hkejF)Z_(Odp-V`eu8*J z7aD<;n+5|H&nBC2b({9~cG|ON&yk8o1r_O5IXtE6ZUj&Oq7S@+lPwvN13&)pk2!IW zxB{tu%Pr|i*@oJ!$3Bm$Iy32yVv+!2s3(ICnt4>V%;W$9#nzfODx~!)NWFlah4oNX;bo9B& zZi}xSV4Xb~I6c0ElL!FkC!UF-8GKCPf*lSyY^Cb173hG#gR!%@j8jaaFjZU6(pn-w z%?82`t^fO1@>+HP5l+JT?yF5HrKH6uA0RIOFuQ(}x zs}O#c9Nv=Xmnnnc%ddL9oZ#D(C8Kuz_LFHWQE|5 zwlJv!g1_M#nnPV+wbB<>y-jWdDyTwjhg65k`>S95ioW)>uNnTMAVE|0xF@pH`FWIp zmUzf@SgFsezf2q|*MR6L+hOe4LAs;B2Zh6w@ep5-L?0V~SU|!A(nskIXD{8;7r*BK z$bKwS<5Ptw7X_kYJv|-WR$!llkm&#%e6b?sqV8|AQm;op(cMhr(V3bb7g!#|YKIaF z@O5Q^zd#aw^aAOR5J@$UD|UE~#sZ6ljO>e<8lUYz5KcNII)LLM#TGz9ONip%J4oeD zmskH}xr(l1hnpf!hXL9yLH$s2~~GY2Hk!7_dGMJb z+MOi&$c3=W{$cCtqWNq#zpkw{r2{syfxbM+WwC$#>tFQ10}t@Q_Tq~#((ix&`@uGa z0i~o66=yP1?~Q^0>~#$VEv*(a^|*bSuRX#mkotA9`y82Qbk0fVQ+|FPwKccXy+8c} zz4+{_YJbgAN%WBi6dvTB)>*IOsOF?81Z?@?Zg(hkG(-tF6xT6R1A^wW7Ymwo&8 z(V1tSIcOak;iiz5c-6W^+g}XubZm&#hx)$1t&0)eoN)gTyGMP@M%Q}n*aChZnb{=Z zk2qF;pr1`*`ue+L2~!=5f5|tmpuGIS#IN1FlM}viA1`KesY3YEMk66cTV&k*Z2J-3 zOnLfgD&DYx%wITxK4X%u!xtVzsUnw67GHVqU3%jSC+PYfyzUoVZ~blDrPbMLuPfJA@0rG>tzs{57*BFYW~IqR&m48H;C^tcZ= z*x2z~_ct>eJ#m&Dc$`dPiwddWAe3GZS==qJx-N1 z6?EYh-x5)hC@gZQRNaRkh;N3@`nH8hpTn0eqnY>KD?}1mpLl|9T)*B>k_)MG!}`y& z>PA3SH8PncmP5`R2*xb>7)^5PK|iziAf&zgzD0i`h&{SUGn#!tgLL{-;X`;1k*xd zFE@JqUM|U_Ve9(sobXNc&G_d}6v8i-M9&;V`se~QCdArkWc5Z^k!AC`jjO29!R$$-Zl*XIwJHA!^*606R8@ALQb zrMqMi**NmjYc8VeetI>zTth4OczqsruEZM9(~mCagb#$%Zn3@!V?TGKk!B>>=&1Ox zhTGv9(??%C_Bi^}?p^ffJ$uNO1`^~kss4I%Go7NL3+i*vJ;ytyitC>;Wy-*3q9`L? z@plZ^L>^{%snv1qP^%-$M@5oA&qNZ<8Hdw(11MAF8=^f#I@s_Q8Em-D$n(Z>#UTeTm@3DY(}h1cFIOS^yKMV{l;{TffY{b8NDc|ECw=sL)27p-^N%8r0rBe5Z1rDk z*{sc(P*6aZTyhC-mT=WoS8>m~06fGXB3?0IQC|*3f;z8d199*QM_)QOsJT$D1F&lo z>}z3mFQl{0ZXa^J$S#ZaJ3Rd%%FS^MJr+~v;dPIf8Vijk9xnOe|I&)L|3(ugk7pB= zzJRGbN)6!=j)SUY2@AtFcZRQ0l1=fACffK%U zKQFO0P!U)XT|Y!E?^97s00c*IfmAS&smqTay@32dV;gtEUcG1A*2IIJkyuPrsQwzM z`l9KgC>SY9FW1-C53xIk(-CJ<9A=cDxZ{9>kr5poTeJfET9P0-`pjwdF~FZwQCUuZ zTlpmY;JWXTt1tHYIy~|=i!J7Fee$7a>32W>qxdJ5;@Yy+9L_eP4|L~%gaR?DG}ym- zB>Q)!Of~$X|C2(W@dMJ*G-OahvZGI1qQMLGHWf#no@>NaVp7o(r3TgA<<+hxBV^WU z*6&Nv+DZxuF6yM86HY#!UVLXcEjeKkc|C&@aQd0LuBCTLT58Z^sjSDNJ%bHtt$IE6aQ0~}lO5|mU3KFR==s-|QR%o+?)Xz~zOF2~ zmey9bs5Y*R3N})r-tE zoe~|+OHJBBPfy%{B4{ZOKXpId`{>=&>+I#uL_N-nk3Rl@j$3j(ty#N<=FB;i?BU;m z&bpX6CXZ#J5A?)_M;Zi=wmgy!K%y^H1y&e=e>Bu5lsu5okmhi{sk@&fs-uR`Q>drg zw{Pd(!c=2d#-zyF{ME+#{yk6J6v6g#ywQO`R8Os{As6tg&tY@Fn!_J6KLJqwX|;Q4yHwP)3Z-6qiIEa zGMcPN_s55W=$ZBq)+!D+URp_S3nC){b-S36#wZoul0fA|jW|&!PTebA43HvjH|edXU#0K=_6EBCwrl7I-@b-=x_bw$<$HYW{Pr9|o&_e}i@$p*Uvr-_Z8BL~ z^XRadVaG2tu8CV$Qq#EE)L${}V#RhpJCjbkOc5PDuCZiB)2C0@yyls1{NJoubk~+G zhUin6I5seQd0v{$4@8Z@-h?E}B*{6^`}+sgYZp=-WD8e^9ZY!4eQuxj<+X5`XMK)1 zEkwsPY9adi4?Y_-Hto3%di*C3kb}KGvg`rcxoZ#Ibj44}k>gNEp3C+2`h5+w;@Ou5 zzIX0-ze)M;K21U20f%Lvt&s{=uMGPO&bgC9xkV^c8BjFbwvn#7e_1i;uE33knw~8r z2Trc6BCkQ~>bQ}cm6~el1_&hzO=J>%-T))IWf0LpH24fee>M@`8dkMUQCl;`Qp2Gw z=vgGxYIkdiF=%gW9a5NrUn`rfpt|37=2i5;Q!D9(x1OcbzJ3aY0-*t4MR#WpZCtl` zVC(I5l6&V`a&KQfu=zV0#lxfA=Wn5dr;HbZ7Fk!DMk>+aX~IZcDa!8ZX)&BBSfWUw zgo6uCBQ`mBnTm!bwH{`p;W#Kr(GiDyOP4OCXPde>+%+>jwNirjy?9+0o4-TbUm$<0y+y^8j;ij54y!fgGh_( zhL$Fdtqw@x$u=f8vu(S{?QP}PjHDgz!B_g9i`57?;t)v}?RN!ucUKQPwtdhwX_3@g zYu$hL=7&t`X()POH`9Ng`8U1s#7lJb!?#jV^*FlxysJ3jV`(ZuuZxP0JBO;yzm;|^ z{S9rn=MFZhr+qs3xy~${BeFg;NBajDa0YFWb$GDkgnenCj)M5C1K|Y_EPV^g1jmhhGtd zV=Z7a$1yR_g??_ZtKo59zI^$>-=Y3On&@2**R#J7rKFI#X0uub{aw^`8&DBdpNVk~ ze}c`CGjPpVPQ|r=;7v_Uyt}N=<>fusqW&g!AcrF-so#&^p=H1B_AV;OFVJ*6Oa?ZG zogTX89x5y?qNiT`6RlhQIo)yn-ISYmKzj)F2gp`2iKhPa@6`U$D*Eu;FOY=^3QJBd zzc+5CA>}B!{lVuS2~S>^-^ON3JE|qYz()xqXSNU>Q3!l(scC;JEq!A7jJ;EkB~2GD zJl)f_ZQHh{ZQHhO+qP{^+qP{_+wMO5{Z5>l|LVWV*ijW#yO1j@bFBydnrbR48BZd5 z3vX~)Ge6@Z$Vn6Z{tQzCASZ~<3Z@oKtX+Qw&^w3HJMg&<))trXd3E}9>>0f}MdZ*^ z5DE{cynVE{{xs{OAzG@F4c!xv@z<@_R_HY9ZfaQxj{Ap3vx!V-C?jK%we;AMZ+{-& zEEqgvGlB86E<%2kbKr`1bd}&DmpBudEb)dxoYK_JD6^qod~S zP!-GJ2fIRVCw;{@=>;HL+|cc6_8hl|_q%Q$jho`I`(@{_m9y(6O~)0JR;$%|j~PXF zF9s_8XtCpbn}3-$-07KoUQ6eE4e908{)`bRB(Pxym(CSY!kYHtOjC`iS~^QSxdv>R zx4Z+aZd2;#;{ktxcJ?bXmERA)`|&^#?*T8uAxf^;==)iZivL7v>Ba&6EP-=n%7Vcn zCVKO)h1WB3_iEpY*7F))gq`n0R#y?Zqy*utwhg!88+exL`EN5cp7DB@mg1;?DYk}% zs!1UIG`}0pI!HQF>UyhYackCt$PtKoXT_c=5UmLd%cYDpskZmJ3MZv`l;!Z`W}fk2 zdq-t;)53ZluNKe!dc4TKPFu>(rp0n`#4RJj&eOI97U{cVSH50}{#Dl%0hdA=PuNRW z{p)dfXuTxwn6=nya2y$Sf$Dx*(c%Gl4MEq;WK5Tay@fOy-y>{pFhHh zJZ)v+x<9_j*QFk1{GjSGum?dzA3ZF(wb#OC{TgFxIZXIu>Szj_&}U0^rmr5?z_}N6 zC85N`h6v7i;FlcR_$r#z?sCw+X$*^z+!e%<+)Yu+&j{p_(u- z``(LH!2G?g^eV@j=Sf#CEEObisy180ldQDq+Ol%KjZxe^3!rPKWAt*ST!Q-Hsj}mR z;GL>ZI>{ZfAR-4A{hVrg0zjnZ6Q|D$^JREaFI_#!CwN{*@jjE(mFHb{yM543mfgDI zjxPO(tfo3n+_c^Ytc_?Wce2ewuF)8UFOQE*DPu`KBz8KJkCMNoNWj1L%{w?MZ=P25 z@Sk<8x5~AExl^VXVJO!UX}4zrSZ&;cPF2(9qAo6A&oQu5s(H?alaJgE>w3Y12=U~R zi6S$vv1aE>|ImiD7yVIEcYa4NuPTa~u%tRm`|(Jg`FpVS_aiMix~R&}8DB+py10k0 zOcB@ZtP#@@M_x>TcDyKY{yP87&pq~J=lR#uN0;$Yk#r|H_|6Pweu z2ajsJMJ&hsQJOB{L-W~a3aiz+fDW#m?w!gdw6r$OUUE9m@m=4vMiOw+AC_Bj|0V65T40Msy~vW26{W~vBGpM# z5wJD}AYhg`SNLvSrz>en)7aT=zhdZ!kMF>fxagD-bgKYv>$;^IgCK+^67Y&#_rB@V zspV9SD=v<-CB=Uq#S|dj90PPsOi-Kk_t7mjnl*Te;6da}RX}=sS@_eWRS$(4nT4ET zj&8p8LN5v5v4R$T4OV5iiT7>)OPsCR*Y>Ql+l6WE}OI^_8-@$G5N&$;Dkxr5R zBPsAtlXe`rn_jldt89}0=%oVJCSu8{rWbn0tws^o|GlPVI|mdI9DnREu zPPKYY^%n}Bs~D1O{UCYrf)V}_Qx4V^7@}?hYj6+G}$E?mpx5qwxXd1LK9}fab%BUB_xb<#bvwBH!GPr1ajAW;Hvn zJ1)*%dVjtMPNBgpS;m`tM}<*AV+r>&A}$qi&cp9WmE+Tdf3_9HI9;i`8yw(iGc;xC zU}wl&LNIA!+T~k0QH-&IzO{mLPjP+lO1F=xTt5QT-EgoIoDT0Vg=1mqIj>=#!Oss} z_JiJQLnsQFar&*Cnf|OG>46bA=ySLN?f# z@$%knU0AeYzlZC7TfU3O5@e9}E;Uo9uGIU&Jt2l5{y9}e0*5xAJ+yS5rA*HbI3Ly4b{+( za|d2C#j22VTCG6}Elu01wywKx2MhSYyXwJ#cJpan)=#N4%K> z25>VdDn5!vft8O(@Yu-(8`l(f2bBE^o8odM@U6EeFOpq49z$$1|6b*J7o)ArcW(ET z8`QsIe?m3D#Xuq^8F>IhMoYk&R5lAORWJUQ=1P}mknq1955_fW^mqN$b2d-aDC*Mt zTzNC^FiK}SWK!9o^I^bP^mCdsd)Cs%Us9Ld9B(RJA#o+t$~)LvHmGY)jI0_uWi6eV z_o(kULm2}}OZcZnW;iJJhTY{^QP_(;EfHJ=Wqx8h=J5)2(cQa!#ptmt-Wr%Me-c{y zb4&@UiGRoL(eiRkQ&n{&t+cuLP$&vmBk7#^7i6?da@z=XE+q>}i&>sp+tWw;TY#E0 ziuZzK5pAw(VVcU)9I>CzAM|l>UVAnOJuffTw|FRZC0VK1Tw&x&)(u*li-=Y}iKPb8 zE@#tefvU0tOzTK`i3>{299^o}Hf*d1nDYn|CS~X4%qO&?46Bo5v`Yqs;6Z(G{Nd3_ z9xa%Pvk*p*vQ$+sbdpu8IcE(4sb`_q;FH-M2VD^)-vQ=iJ@oSH@)|U?=0_NgFMk zu)2W@@tl#SIH4V{m2VeohryV z`{mr1*~G7{H2c)H zjXzr_6_`PRg|V<|)@-CP3tWb3kr`&+T4|Kpyrp4ZKS)VVs)n-&D8Qcpt0--!G4(9v zm{f%JqOgr`;xF9ri7yWPJ&1trwy>eA^5rjW0D+c z=IiiOg^g@*cd3{S{%wFf2RKi>zxf_%ppIgW^WFpV{2mBOv*kpK#^O7-;NzHRyeApd z+O>N~Fo2(ACm!hHF78l0IZDOMY8e6N@<1o|#?ZFoNnNAa5()no z;Hxo5@Ao!I7$jNVWM^krDpB%g(K9M?3<_s?{X~L6K=$tmRFO_$Sc{&~sbJMhpo5Kj zn}v@5%W5h3G@PP!R=}B|i|?UDp(MB8QbG?=spR10e#`FUVIu zAu6t*fJ}!BAcYA(S|n9ktoD3`D%a9bjgkJ_3T{l#=p}}j$@t(MjFKooGxaYeyOh+ zK!KVvUYxieYQP3?jn@<)r^`UM1SM3IRYV|@R3 zru0_^qM-BMTu|9U3c3&NahTifRE0f3Y z>J@FQ=!YeZ2Nm&!lw*F0FK?|&t!8_& zr`);m!h$`q5l6T5mV<|VpZ?oJy^Qm`eA1z-q_M|X`Ly>k!nUb_mpeeiH5!<_PKeoN zcFH6JORx_vp^w+Ide-XDZ%$2EIY(i??A+xEq5B*ePtGzb^MzHA_uzyxrFJWxJ&cuK z`NS$*Of`!>>ic@xmDBs!SbY@`n*Ix5M!WAZ>q<_TN>xfLRbqGKMNc}(>k+UNPa#Xn zy4;m>R2iK9ZnwE~XUiFNuJTaNtiDA*K!1h@E1$JIko?8DNgVms#djI0J>*|++@;qdeU zxLJDY6xwpSiIUk1Zj159eqD3sn?j`G1_;7=L1!NJblkZi8Z@I(I7ul3#emQ-C#-cs zwnGuM%^LttD!6xdbx!E#%N@C;)#aX5vB{8RCX+7#@r(2K-);QVonhvP6J1GVK09@A zIIxJmU`XUfOL^jbV2hb_7&t%MXut{^0K{@B@@#uxct4%L_6DjcxS5Ko0(Eva z#ZIy5ObCIU6%ARHk&YyT$nko1O5ytg&!K%ng=(}gc16Dcos>n#eLqB5O-(Eco&H41 zhX3na4*arjUo~zp#7tT{ayA!dCZ*28C5aMa3@Rcvb%Lx(V}@otli(iB80_I>x(J^C z)1Kf?u%^eNCM4BXnw~yChSx*_eHCc@vL@K@OhoZ8#4f;S%c58$@Wi9ut!vArdj zmgfz!-14+ARf(jz%=I~Mg_L-P1XfgAlZD0Y`KowU8KaYU1lUxBD!>0**Kfflk+e95 z&lHdbu$x1cL^@>9|EWg(Lgs$1skg&s!TvWPSSyXWGDQSk0VE)| zxbE*fN=8Vi;syx`DdGJ88jB(`_`Pi5KZtdk!+4?3_KV;0y(iu6ZFyg2D-HZS+ZAe# z>|kU{;cC`0V*B6{&Joc9M{I+WHD$j7Mh{D~ajvFc_wL#*Zncdt<>4z9&Pz^RKum&j zmA6NU9^-N9FSK(Ps5uwgQOG6eBKR!hBOHbPdn!o(e;NxC&#q7qh`MNSWi1J`h#u^l zt%yvDVn-b$q@u*LyNPcSBI&MKB~l|Q5wZJ-;9!!D;QwzU(#TMKE25=^osyRJ@M7;i z?Bu!Ml-3Y>VWHeQ?i)n4ZEI8s&+7(`np^7)m4RTwS3B@uNZBzPJyA z+-g+MA|vwdxV8Aw`PUZuea>3G=Xoeu%mh1rXOOfGrK@gNRaJ={!!~NH4e2NHj*$S+ zy+iY`R%G;nSLWWZn*8pVy zy{)9UIJS> z{9gO_M4Y_P8l8l(G`yB>!oMHhafnE;kU=>fKhF-Ja=|@QDwD-_^x(d$0`T{#(lhem z-*Fpel`@oywpi{iEggA{GAd>ozg2>Sv?!7i7YBgU#l~{P zY?jT9TfvL=>Bc+gvuf?;2422=SM20K!Z^ndNCRC*v^1M&Ji9^CIbI84I~+m%&cj%| z3ML>%=KA!R`}~Ow9ds?)ls)W5A(j)=tTcj`zbZt+6Fh`R=jdk-{>blz&$Z}%+;gqn znEz{4gbfpkr!-twQIY88gyGk}*PCZ&1v`SV@feEd0c(#tSgxRZZN+18YY+DPTDCw$FW6l!XxLXGq+^PUL2?Sl56SumL20hfZ zwx)i7CGv)@Sj{pdNKJg^rzKF@xNk zo$XOG1H6B?2mu}`e`L#?oGY#YxleqNDG|owI|!Vsn}ne_)K0#~-QPE_AI3)8$X}(- z`>`8vl`kL^t4zRVR8cwFz1h;ScD%x=K|f~7iP)OB5V`<(5I}AI(~f|f0B31?ds~~r z02t#ANoT;_+s%iWl_4?i%-qk5&*R6J@$K7Xu-ubPJu;*{k|W09aAa7APE-LczN@V* zEeXJ{2pn9-FScV=*=Ey3@|;40s@9Xu9qna0@Pj%Kj!%Rw4BBJMl6i0wbdB$+1IW=l zC7u~!sKfVK8&Mx?i0ZBA)|()=|J0j{=yH>=L%`)<1H%tVBE7x6Y1f2B?yj$MHd;F$ z6-BL2pCPClJcRolGS353EkJT43hQ@Y0{8YtBy90`Dme+ym^$)10w4SSMK)nLt!=U8%z9pmg;4pH7K6L7s_BlTNin zI}=e0 zmF~O0AF4a)%x!K9h5h~aGPF$sehqLgn+7(|iy_+X?l5}d*k$%l)yd%%d~e^F4KG80 zW3sR{NV94CLs~ay5Jd1zz1oFkVRZ#Gyd6%n-Sc>i)awSS-y^{_DtJ}Ja3L(=w&3Pm zBdF8vR|MghS#*UO&(HS!to7hwO{&~U#|k#oIX?!)II4kQDlUk&1=jD6fwyLuD_?DHuEi5F$n`SdjCNF|Rp$1_~ zp`E{L&av7fkkAWCwOWdT?dEd?6(&IpeJQD0@ce`;3!A$1V0C*@sOt2))R1rthlTg; zu1YGV3Hv%Ac3Lh-_G?Rf-@>)vo=(0edhbtzE2`y#?{0U1|N7p|QxGmw0x)uX+}lg& zTae_6g!JI=npe8;Yse>mYV*5c0TrnaxaU*-Oj&eW3dfPpjHT0u#r;F6v+Wv}$2~v3 zqA%$QsX{4?*i)<=qYp55ukpJle#IS`&WiqZ5m=c#C63NH=@{CqMMufkc#&VLtYvsI zdCM+yM+-cfEKWS#Ee`a&pYK?f7cSV2wbs^LhU&&RXm5W$>mUM#VIp53_~b{y;*Js4 zRIaWQG8`>IynT#y>><~C@2M>>wq$1BjM&e&V}qsQeLzw zYB+{xq#{Ukv$iF5^gQ9JwVQv$Wa%I{_rAh0%zFL|zwhX29r+9t5zVhF-U?>9CmjI7C%HG~kn-#vanQyCb|| ztyroW(jh#vX_b}bEyhX&8b=lg&>1Xt*3OGZ=*X8*PLskCV6gCOpXAB*1zURk8dGL9 zP8n19R`E(WF-+#p%gSlcT*1__%2T;HFAw< zoyU}RWD5RS$pQ#79B;+=lklQAR=(gm@FWD~?}1Mq$BqFwz4)y&bLl}IklWe|A9KrD z&kn=wr^~dn&WgQuveUDc+26VoUpp5^e?3{RtaN8{EFvy%hh~_Wv0q1uEy)}|p|l^4 zi_X7nTs}Dk&HJyx5Cy#(9F0wo++AH5Ww6!6f3)@F=xxf47WglHUNUIE1RW`{L8{&+O1?E-Q-b?ifct zE2BJ&eC6x;ehd+j!sbP$;-%@XGA${mx2u=rV`J~_9R)dVJxc!JdrpCA4< zJepM(Wa5mZcD|QksAQs|6)d z0}sVzpcQmyp3agN4U?%UHMy}Vg}1smkH0fPF-G$8R=|^?g7i8GqLR2VAsKDp%C_B5 z-<2homD5WpRgiM7)-u!}#m@G*arrCO<8ysclpXc`r*u53yw(C&DdH>p=p2Lvgr2)@ zFgNe>mt3Kym6;m)be^I_&SqA5&8*b%Tdd${l&EV#t(~2D3G%6QDVFHvtQ&hG=_p(9 z-T)_t2TazoX?Ynht%zC`D-nq5TJx=TWsVCG2pTq@TG)^%bbv}&izQ?LsQyRTpw5-B zkiG*QhZI6hF`T2g8pI_T*}ND{muzG1tx==jov5Wn73r!!Zf-`!Ci_Ag#5vu{ULOmKn7O zYu5tyN6Wz%FNq!pL=$h-fNbJ_EHQ#)C_W{HN*2c}6LKh;Kb)RSqz`5LhP+xC@ zxYD%rq*4DcM;bTZ%uJbQeenM|-Huwn>O;Zr1nl8Kh!GPXM!Q>{F+MPm90OnysSSaT z#F5Oz6+Iwsl+2K>SE5XQW}H_QKgku19{_uFsg0XcIldz49GDVR2!BK=Z33Qb>s=k5 zpp|787{a>x&5*f>5AJjlxwYj#8o8UDZuHBms}FSzHAGtBaM~5plH9IW8^e%!Q!;op z5HTo)#6FKdys{qb*TwHUV3wA;{Fi*O`V>ve`tiZQfmNieBp+|krSoPkR|=%85I9kn z10pg<)O`xrSlDlgO^G@lY;ZnH3SuoL$n&7}1hJWl{Q%*WWu8PT@0R{cqy3J$aY0nA0CIJv^&vgy4#pbR+UaBpbJ2KKHC;$>qT7^~Q|D^X zG^|V*JFqf4(g%+gtFe$F>|_OH@%mXeQ#o^X-8>TTY-r&qbH!EInQcf{x#x6T@Hb0K zt^O82MDSHjNzV$!pg(uYn-B?05AQDLiB<9^Xcf*5H4N6h$JX%4HeW>X$nm7mWJUBe ztcvl@d@b#?d#zpGIuq!MyYjT?m;*H^UwIr#dGt+A-Jv?aHs5U-%`AJ7O$*vLV3Ck; zedF3>A9;3yj63*KPNcRLBt}$xc>ft-@AuW~biL|p%;XwUF6S^*noUl~t5_~@&qJ&j zRDZ(N^2s(|E&D@?4e4GtDf|a?8M7^f)e1-A9io|I%y9j*!P>GXa!6M$gFQvnyCdiO zeiv3*a}Oi_B!2HTSN?EX@?V9z4)8fG;8X9#mCkZ{)GM;T*We|6HTh z2G_XT#^y5D_`>E;7Un!+f+n6-^YWN9otadkmX0&uLe^LwkV4#ILraGFiLFW54AS+| z^aTeP1@vbKbO=l_M=0XSoQNcaR%Z86CSIhgZFL18ea_%- zUd|mOhxF`?fk|{XSRuIPi$a6VBiCWI1|CgNAciZqB)Uo7dv0_oP@*Mkl}Hcg%4|W$ zU_clVDnT1pxz$|p+(p41sfVyQ+%nDwu|C+cC0YuzOHY}EH&5Dt)xqF`x!qr*Z8;Fsj{>2Hb&c(7N+u@RZhC@eRCw}6C>p%#SLq|TDqN=^A^oqB;J(z& z>7{dcQ`-509>0a-lJWN8OJ7qp%70rpUq17<(nTRX2X6NP5`CBQV~4P z=NOe;8zUoh6_lf)NAh%U>FHhmx#$DW)@}C70W#=Z(rC1y0=)57)+~0rlcs+FEkAHu z-Dbej8-d|Ml&;7eG641FaJVCj25<#4boGk>Z03aNXG=lYxBVE*#%gmcg_WYc&{6%x zO&l4H1p!?#HSx$rV;O{!iH8=OoKMVkzq$|rrbsAAxM+<_gwNwPmq z2y~FkxU8E3_6T^c>{KYkDy#`yaT8VlGJgm-6N|tsKrf%yXoQ)k{uWJ-R;?aDd#sDx zTN8Km=|JuC4@5^AzKW_1O|h6Bty9DwJJgq(wH3C&ZoBDINZu+QUUEh;l|Jzz=Mt=5CLQgr~9N)NstqTMH9Tcq8%C~79 z_lhAO^qL5&bc!gI6|8JExj$1aMJ0y7N596_)F{eLIA1 z;){ZHx%%#^3zK@<2P1mAca&7*0>iN?XDPOFq@==vFjDzF)4q|@c@}Q}2D~qj%dZGB zEgrSsrAzo}iW2L;2s&w!DWvZb+8Q}KM3ZL;arufv?I9{~v@5oaM>ST5@f@!nwVvJ= zDc-3y`JLc!4{<5<+RD^mB!OYzrb8UTLCmVI1%m&02vn`t>no!DxYTt?KOe^WIX;0km+v_OF2-@^n zYDhxAy(yz&!W=4R1OMz)GeFUo5emTFB@NExa96aqWBzQFY^73*F{qgzmFeM%Be(-w z<%*{UNan?qJE(i~|5%UWwdGkq2O2;}$dq6h(-2gvL^;_Wj>Pbwqz*uniVtNnLUAww z*827h_g?=PvD=!PDJlZKBq6HPs*L3a^;g44#W+CN9Fg-ry7tu)x>0Nvp*fBlanC3o z9MaP69i^CkuMs}#8ogCDV{vCW!MSlX3lvpey-jFXW6&I<$qJ)1Fh+0_U*9w>1KN}z z16*K&Cq+a1L~Re8I*C7_;;P1UBexz8CQ&_(@PL+y5RtT0*kYJ)f%FZ3G-%JYBBPyW z1U-ZH%9ls#wddjTa80@}Ttv{<%6RIfr2Bu-Qj*^hGy&_?&CI@IpTU4o8n-Gi6!kL` zR#s$Z?L>%Bx)XhJsf(KU%f{^mIEhi>i_*@6iWGFGg;=6tcJknGlywkXlyq!c7xuDq zRut=dpA@%`OS@{>Zp$QP*=_b~ zc~-C(FOCi#3dNWuQ^n6+aq5ezrTXKWLImO(4GJ8MTKF5nEzm9EB2?XW9-GyKB$j@1 z`rX&s)HP)0TV7Pi&5CDQ`KTiJy8bE< zex78268^K=7(6^h#LKdTu$Dd_5&aHwq8iHyF%F~O|B{72#zsTJE*w9MlYOHgU@!ig z$nVP(l4B#w=}kKS1T5u%u?;jdTO?^-Nmzd;Z^^uff0ArQx}IlyX7cajF*!<%DAi{o z7?cwC_a89<`;yc>;fdnUQk>evvx;98_7JU{!Zf2L%($&frUfNos>V6fJFED``Qwrm zLp?f)D?<3mSN>2LFFANOWRSu_x|+#~ zYOH5-UbJu!(II5maPxPeLRrPvUhy>eIoPYZ7|t(hV4S?h5ks=j3Wz{s}G7iySwI$%3;P;HBQp}jb*W;F><13$O=gH*nYftN@m`D3zVQodw9t9So z%fUb~uz@_7bP69=(m+YJkfVDETvZon=z;Zc=vFVFl3H7b2N0b3E%OuIXKdQU@1Txa zb9J}y+pFXC-HWVg4*{uj5j9`ii|_3>IM=FX4O?YJx)fT}H`8=Fj!Gs%Bm*B!SYiW1 zNe5jfZ2PuGq%cJ`<>>0>JRBG9+RkZF*t4sW8-aK$%##6qlg3$XhKKTMbFSAS^(byK zN`C@X^d~SVMUi(V4qK1|VCFAT7>A%e+Uj*@`ajW_lw7h<`BVJORbhNrkH54wGRWuM z{RnyTc>L8&=SBp10x3Mftdd(Oc{o&niXD($)0Ie927o7OH=jjE(0D!k<@0FT8Rd1+ z0A?!@_=156@TXpl;8f-6W{QiYm7&E#I4`P{r3jbMqYeutc{l&9zA(@Vj6#vk&(GYB zUh7LH=;i&$nDk8Bxf$lqxI)jBIvk3)VT|8XJF~XwfFhg8zRHo?Gfzh$iJdeW3U`60 zhLw4ku8f4CSQoeI-Z+;0O1cRP>c4RVHnpUH(wA}{6EYOJHzxu_b+E|P>}=V3lbP99 z$l#|xyAF^c_{qd5rHWg`tGkTCDL$IZoV+=qRzSQ;$PvYZA|aqkNLK)_MismpAOw9b z9VP=ta}4 z30R~h1qpaV8ATRJJ4uH~rb7?WewGL)^{^@%O5rjKU@ujNM#2da;Qz-E5MlxcU?}F~ zp>Nz|W#OiCvN{&A|1i5=Vw{n61rE%xK~m0>h9VYT-ldo`06}*7{d^b01z)qg_5iy7 zoX!7g!0RDlr~|z#b8~%=Q3G(AK-f6ACa40meVOh8JF0*yGbWcW0mVJc1dQE?s*{Tb zGK^3P&CSj7IyyP8UB`7+@sA{I#~sN3K9ebx+<_oU;DDGW^5xXeL{V__MIwn}zepy~(<8*$7a z8U2oC0b$GjLK@G0lHxEN`Yu`9Eo_+0t&z(L9Xax68@4b?ft;Ns_KqMkHXi%S9 zTMGx6rU&a8lKt-74jD9Xh4dnNW+SGzMLq;)h|IJwH%Gx}9@h~rs=vi!v<*PK=YX)> z1Mn@uC*d1LS_k*VEsSU+$6ilCSVu4|o?sbsSIe}8i5c&wk7^-xgo%6mN6=Rz9Z;it ze2`3=FquuK1YBI6;^CWK#kqp{a4yk?ZqL*YVL?S@zonyp#4rW)kYHhD&NAm%Sri8) z8GYf3;7lQbI08jSW2_CETW&bOCHl+?NUr+2O^HxwzBzPfJ+X z>P%C3b#BZ|e)yXwTCs?iwz`Zw9gg@#!jXG+B(Oa-qY zK$HI*9jvKYB$_REfj`n#78Lx~FA*_@M)cXhS~3nG0sc!HZ=6u%(XD2`(;6TU zj7Xgg{p!G7qcD$$WgB>zryLwZm*$Ls;m3O`hazxmba6V(`Ak<+r3^xzPQbyyDBN|> zu!me3dm@8yj{xrv@Jgi>=hmkO=+s7bKS;PvLXuD_a1&7;ri*hSnvocGwIIPsG~ORP zU{G+}aY#g=$Vl9LeF35zjTA67aP5*d*swbS5H?@B$?Z7xkoS1wmVNvwTruHaC$)SM zXI*U2=yW)rFrZke#js#OC{uWG-P1yBy$FY%XRh^t2x4U+1C<>acls-xgnz|J^eR*qH4u z+9AI_3vU8Je?nhXWhL3LAk(i{&DUF=7iEF_BFx+kuA;}$O)Q7}QCL?qT9=K4NU1#E z$3`OX4qiUz&aZMB*<2$zAeoe6Qu7jmerTXGh`S4?hPHK)lQP3k1W_kU*cb_*CO%NC zq+iy|6RNqz;GrJO;uAB^c`SlWufK!ZuiH=YwtPRXUI9Wj8gt;2hX?s7gh>>xDp5Hi zGeq=4S-7ijZO`ReYhS+3HNyU8GK}2YeqtP zewVo#K?XIJtSrr;wFQiPdzDj5B{xI5qiDWK(#!u@9oGZQfzI_z*XIF-=m*DXdZ{F$ zeh-15&`60mRn>Ro(u2pkng@)hzgn0$jf;y8yqsD=QhN08e3BHdS%cMaHcjx}sAr(qx zaCqcCH(g>woE)m3?~<2BJLxCii)&W@jqg}k8Y2n}lGQ!r-!hr%=g_LYM=lpD5=^tG~zIRc=Q#YTnk>90toN6UXsOzR!7P2;(D`0r^kd_=C zRk7w3P%tQtuj?x-s!OY@WyS-ir_8A0?_gcGV8Z3pP7!Zfh_NE@ocx1dKi(Rb&4aeA zMUz)SE-=1+wt6{Uc=Tb7o(%Ofhj;N;Kiq8Le*L$Dy(Rd9|GUtPfGWD6^d9OFgwz81 z?0%hydh2y20Nm~npw5@F@v7Eyk1FxLWQ_0k>Cm38=d!FK(W0=7=gff41s=?os%^g_ zM82=EHztY?W*c-WmcSJPYI)I~=_l3;7=_PBWxcSRf~_ru^St6V;p6xr(gstPo#cQl z?hb~7dKVjocRTwk47)0h5OJ)*vFGB-|2M22sS}ON4slR_%Oujb*jpx5__wd%+!09R zcwM)H`Z$WWMjxEK5%il>H6%v*mk|s>xN8Do(4{bp(gnBJR=`{_w<{KX4Dr zVsj0>e0T>$L-69t>njTp1D5#Ej({!Ibk0iF2y#((u#9Z~#MFe5J3@PY{3nP10u64& zB+4syD!{@ZNp+2LY^MIswMyiIZeys4L4(UIPWcGia;8A|{^W%h9q7@E*U_o=`oJv+RqL8Zz z;W?{;s=qWrC`4{~7C@#S%DhiW+%yH1FNLTeLWs|vUiVRqZE;KTrwSedVTzNcI2pbh zETz+o2y%mjLW&>|I~}%4^YO{-;y4HkEHOf?eGxwY?O>;h144iL!>D+JDe?4$FiR$dq1{sBxRgluD4&;XMM?7X8=(si-aO^{;~Vh z6KzgUBF}6Z%sKADL)Aqo<=+Sa~G)D;{ zFLjgoOX%kAouq&}ETt=E11CIrnOKn%HDG~DSs5NHlMkhHwWz`C$H}vMc@C>XlrRvM z1u)C`gRx?ptm#h2Y7Ew^5T(;(3ahE9NpgnW1S-hB?yxZNHU9$dr?v}^udM@mj=ZoWA-txpWgLjBLv&h z<7QhmzL77?)tDk@yGC6ujr4J4k@EUR_qLwD|k0n7a}#8N#l2! zMrWf3l5F;W^Rc}S^G7+rM$b54{r6$GhbqPJ_<=y)vQ7S$wS1rACf{ek_W|fBr*k@y z+5X)JlQ=8O^L-~Yrjvne5Mcg38cQLoZq18D=rbbBh)bm_J;}amXU2#l^F!h}tv%^? zk~uKsW3PJ<|NgR#(^K^U%t~V=Sj14B)6VQn1xsmwHg_%(Hhf(qJYSSQTV$jlkasVh zK#OHeV}S3-D9jM2I+l?*bs=HtPDx;m!%>AF&Go^t0fjc+=22yIzoG{ zB4qTVb`*Jur1$%&4*&ie6kVG@j4<%T)v_NYRyp@ASlmWpEL8}l=ujg+v^Y1Kp+X~R zP9i!(5gn43Ah=Ptj^9RT7Uf$X!&dNY7%ppRF6?xsEN~yz#J#7y8ypx2s_$h4Oa|O7 z@=njN`HWN)5E7zvf}+L(n_Ur01Muwf++-25BE%`m5xK>(i{4^lVhcR3U_%X^SS?%9 z!AQQt7Hvc{x(xWZEr(9tUwr()1@#C7jjLl}z;#HTVJ$&l+SUj#YyIt_AZP8^%qOxy}0T91Ou!C`-5-Pcb3W%fw+y>#sV z0Rln&zJlMev#5>`2)YM+<92)lz952P{!V*tmdjDv!9Ey!o2hV2iDUcU4*g_kXhPRW z1!u#kW4$E)&E4TJ{_OK&cQA<0&YF!Y@|4W!%f0{p`{io}zOkg|4}bVWJn+B+vbz|4 znhfrG?|a`{5Enz$l~;M+`AF!B)JaTq53tlOgXD%BM=*o|KRcxrWo)*lif$KVPA#7$ zV8;u6-hg2pVD9`loc6-H+RVmrBqMZgf20fplk~xx;r98_H{g~8pQ@0fu5mix+&lZ-GNRrTjmWtt zYYY<>$47z2XSWVk-y3UTq~Yygd0u({it|K)`w1m4!6-+|n3f*Bq-jFeIJ|Hgj1sFy z#lN{nXyFg~ee&msL?Z@C{B^OotXlmXPOhoJ4~|%d(mWw{E_lyA`z)@$`s%?&kmPdV zg%`>^b!zZj*yGK@ij0(|$&o8U=OUkKya9dE{CukU=ZetB(#>r&A!DRSaqgF-Mu8~a zCFAS%$R6k}r(L>i%t;8H+aKPXWyto0Kv3+kqrKmQ5G*JY*DM_?0=JuPtcNEMmML>Z zV!u|4IjNRzGKb|oF^F>U>uv5&El47(Ao)=S;t1=B>E@Yb$ftFJ=t!LnA@ z#2)g&)hs|QToCvhStznHAT+CUrcA+imn?-NPs_EAq+P}@`ZO7{6I-qbeYh}@4Acf$ z9ESwKz{6CY)m#($Xy=gUj6Q!7I^!%^||vgm=P(_6~0ri_UExH?HdM!!-l_Ss`;N zF$nGa1))uz^z1YEebdhTG)IX!7E*0^l47oEx91Y76CB+I3iLzE2zP3@W7<0X=vJBj z*5k>&AKe=W$-!na=5eUdmkC%~fu(s_6X58hkCr6EAUm4tV*>nlVrxJ&wkDjQm&eHo zZE~MHD2i#|*Tgh(L&Ff`FJ2GUgmT|6!W1L+KkV9tyZ7wI&yPGB%L=e=dcJ^~E(MTW z)^5)>RKz=l5C$YxS67K+u*e8^_A{sHdMx!*cp4mHJ0w@8!Q~o0p_#KPT}81|_GQnv z(l2x;@q7<&C#>4RhC*T7{y)E#u_NbR_&(lGO3+74WB*Z1gZJtuP7}Ju#Nw#f!+2W! zo0QB=)z$dYri}8GETi{Ja+_rAPOxufZs=3(h?&SI<7@dDihBoU9>gW}Rs^!fa7uypDOKIqi+ zb0gB7RpWZcK|^Qnb-y4*S=|o#!|1#>41uXSse9tLxBdZ-J@5pION`ERo8R3kf6kaa z4X2%R3hEkaDfs^&Hi*?RrQj{Sv}!`v$USI#Wi&dY(ub5{YkcrQ)XbX)+h;$E$68wO z{dMokh;+*YnhB|`ukL8adsnZ6>o430GcuZqr07mD_ zMl_?$J%0Nm#R81Q0>T&%3aT|VH3>hn=9RVh$#pl2d7B5xJRWqt4LTBf;uz1Iw#=TBbwx;pN1MAi@2b6RF2Hn0)l?|a z`SE(Z`2K(X2%Fb$fwL&JNc(UojI}SnEeA1c-VCfd`()JC*HB@&DK-e^yEBS@qh69V zp=+dfO<0+Y721Y~;8UJ{8r8493X8|1R7#+{0=7GT3%`)ozqn=%HuUx84Ve$u)^FRf zUCgf!KUuy!r~6`eHJaH;<^*%oFTM0q{PB-}lmT0pUw(PgKHl-&%5tIQrqT(Gq!Zta zrO?dngXSb5jD-At9BA)EC>)h0_c5P85~n0|7MJl#&A%wc3T~Nnq3ibrKm&j+MOQ(t zt`&)#+fJsH|Ksn^;+OyPTR4gwDUq5}bgsw3JEb+)1=|j4QID{77`1s=}-LfT-3J23M$HIE^&#>F)!)5<^ z0UkjF&K#vHWdp@xZqIkL<5RD{hC9UEi*htqrb}_eqrSc#pZ)A-3*u&Aelg9qu_)z& zgbI$c3KH+@?vMGt%~|q5Q#i0mg6;A6Wx!g_u7y7UJ0sl96X!ptiCxu3NQ9eixE23; z?qzYm6lHWS1Hpg{Ty)x<@;XIh$LaI>@!Iom$U)3mFbk)heF~~-D=Ag{A~p!tnrF@V z?RqKKgnl@J_R33P_^2A<|IpCTApO9~pVUk9>eTx9*aIC@C$LzV{POTZxz=qLdMKiDQhq36*ftSgp!#x#{=~L)7-Agh88lRlOn7(wmUU}?q^>2XqL2G zM=X_(Ch=SnI{U6yl{n?K2uH?`pBQIB&79d7$%+JZwRZ`vc{bWxI}o5H!I7G99EnDx zR_?GlGIlEs^t+rk$KNnOmf)#t+(*1jP$_2b~mlKGwHYDTwz zEt|4Xc%sHrnjqqyDQ`?~Q%z7PWF>0_Iu@;Uq(zjzQVJru zEFU_48YZMEHxM;RlavvRK6cYJzs=w}k~&+PX6xJ!hdU6GB%Y4%KO1ousgOA_Z_!*_ zecjg$;Uf@`e)LEF@^>6)X_bETLjxC0g2??anN8BXo=xo7+$0B4Q(ukuec*IVozcLP z@rl?#jMT4*k$e;yn$Ytclul_zzZ7v*_-J;dPdAwso+)M(7vrWCE9C3v+uLyCrcLM* znyI5es-fA%Je%BZe0$wHxb?`Rv$9PtgdEgzSqgKq6vjA8NzchbHuV0ewdd zXq%TUEj$yfs;!e_r+YvUY`pm5 zi;@r*#J2g$E3e2#79>pmeR#kb33S%1S(0=(X!7Qu>7LpTQ-w(EtgTK$V5~zXS~M5^ zWT{gu6vF+ngy&ey?fHZRzS@&t{%bBEn3v|2H*QItxitue3LV#$12C7E;W7;Ea=yoGb|og2SpSQ-Z0 zc;L@}!}CwQgpRh3B-;DfKp`e1hD`_z4x4j`0={kI4mpV0DK&WC`KO_AMgu4Cx!900 z2#mnGhcu;(!2+n8v!o3=Sea6GdAvYG^DedUe5pXTOAz{3-g*n|0e{|W?7Rd0LIe8A zvgJ}JnP{-XnbN|C6AOP=`M~@PQ{vL8wVtdw3*V=(z=f8CJ{ApnBAc@e(G~>hw)FY% zrdFR`P_X@RS_WU$tIvhAm$*-}{8-r9|>0WU5(aWN`fqYGE*HfQo()QyC1?kYu+`2FYTGfurRQW zK6!@>ZcX1Rv|#Bxy!X6SC@vWyU^5ktDRE4V>&WM|E8m*4Xbn9z(2m98E=%Iyq{)_A zcs`~V(H#HiDW_m{TN}Q;ew{4vlCM5?&dO>snp%|)e-h{m@C1xB(|NGycm4%p~R!uWJE7WnHG{cWn zIyRonNNz3Sn5nh$9-Ay=J}9jC4f3Y>o&IHWFf>>NZ*1q@2v*~E`@Pxky2j{srmV3- z9L@WY99|ci+9}Ls%dgFQib7&lRaM&GU4HZta!7*h>BD39KaM9K`6v4O24J-tQ3MAg zU>uv9GX^3>(U<))aPl6FODv zYhyRVpi_+x70u`jf|_jj)XbTfTT+5=zWcT`ap#)>p7UXMxVv`k#{aCyjL@5#n`O5( zT2i>Ekz7fb+qZAWHP>7tCH~uQzg?1ge7-q}lo}0v&?M*K#G8^?M`&1FOz8{fclkRP z>iC-MBr|@7_n9oZBId}~{G8hFSfG69ksRerr{hD9M$D2F>FP1dxf#BFML0bE;Q|b| zuOi(?g0ao0G>3N$c;NT>MkQT!jYos(wH^$G#dUEQe&WdJ=@8R3w_ESywqD6;V`hY(S zhcmUHIBqwUpt!I%grFd3m%|3Tm~%J`T^-=Kk#16W+n{pU2ZR1*ZnV%_(#+`o}%SO_)&X?mF{6somv zt;M3ni?MOjM(MAwt*sN!Qa|?Y=|nvOxaH2jptRV9OU{2EJYm3Nc0!1bJdxjt4LN-= zn5h5S;X-heJ<6TtnX6$GGbo8)G)Um|e;%&*+rE9caq~vF^3eS578=V%(`Vp67B0+q z8w@hzq86X~&-WDJkA&9N){F@~5m>~Ab73yK%5^808_ysp`cJ8y7XqQDs-cs-eL};1 zV#9t(?sIBN21}=^XmehR;Xz=jM!74YVMB9=ELii26PJvdx|U9q`?dq!c;wCPST?;D zHKj!<5qi2&?z9$Bz@%wB8`qvfBMChdQ+p!3{mg5)^9R3_t{|JmCIbT_LdXgD{HUAR zfYUEJ2S>k8Hg3V|FTH{L{`Wy_+q^>xK&fc&jE+~f@vCNBRzdp0UFjEGL; zGMOkh3bk}<;mlo+Cqgb}T+mZ7F&832V)6Hi#kpK8!X)@`Z1Gjr5M{xLC-qcv&<6=! z{BzhW==TJ~evE$WS-WO}wCFpxrr2dI6HvIwrr=1yFk7)ct-JQ)zMKCfuahEkd40tB za26M#ucHSK-~NC2>rHn`@;>s6=kjv$nNYDIZO zDD>~Qh{gWx-X5Hu2YKmZHiN}-*E!+~%czBXGLpH9a1RR}uqPlzW%}kh;rXCePJ-v} ze63;+MzSDsA5O8rw*}qs2*Dwngx>E73NkkdGRs|YCb}K6rC?AvRDS!hA}a3DhiN zwzh6V#S=e~_hC;!oS!8pBXlAwa!ZBpo!GZ}#Q5)+B3RJvU<;Md=f_U|+3|03fgp)z zGnaERqfSWQ@2juJy?gh{)}|8)E*Y|4wUXk+>o>Z(Ga_{6hVK=}#O?8Zm@{XNjF#QG zbEkZdrtXDfFz1hz+|#WoUXX+?{`mx<^LmmPlR>#7XsIBv^StmU$xa1!QHziF-ShL( zEH5-eht(p>voiRuel&|g$XKhD05KAJy*^mY7NZY(4x+i$T&!8Lj;V$|*}yx`zkw}p zZa{HKG17@-H;Z#h?Y;Xz2OhfhZv6R2zk^-O>6rJQgzNv`H7KjAM0-oS)ZSls<|Wxs zhOrOoQDA+3izV?fFep(eW@ff|wHGBO^dT#X_+G##1%#62t1#=lFCsW~KEmDI=zsPR zv}_Q3e*76Q)ih8M{Ap|uPsX(Ot(wqt1769?V&;+|t*{y!8>JR*wdxGKOz?rmsrY+K zi;ON$rSMGr<4tlT@iLw|5>2q|#e$I7O|3*+A6Bg6KG-sKhN}qGLP}?+IE#3Hin9n) zgg{nqbI4Y(HcMRUL7EHY6Hh#W6)RRqUn0p|KA}%ODPObMN-{E!j}XsQY>5@V>Cc%Gf6Q4zCxVT?VAm|Et^&(r+v78xnhbAsTK&mE-k#qJk7c^8VSrdmk(V zol+AcS#z5t0Im)EodJo;|IdBUluThyUH5naiz!p?SPr_`gp?xO)!eu(m0>WF;FfC5 z`cy$7C0oIDuRs2RG=EskV-xZnG4uHw|9BZMJn~Nny&2ON&c^AVJQrWS`U~=J&px>t z58nF-_U>+$TKbq{KceFN`#i8#)S>dd7mM*}SsJ*1^E&Lf>o@3Iw-y5-p}7k|gXM^$ zO`DAGiS@^H#PKkna3*4k;+EJJS*h;3Fe=yOeLL8EWT>>6jcvl~wf>F(`y zn{mm-7v^11LZM-X81h=qTmbVa=Kz(pR3Lv98w6|3e?yGjn$SnH(g7F4XmrvL|A!Y| zdIN8~xgPxk?m7_EN{a^q37w);|9(lYU|My!e%R6 zlr3hs#RAPJbM7V@CwMSWOa&yy3kIq>8%@>BYM^$@6hc`mfW>cw=Ff7n{7Np29TZry zRRTAgL6!Tq?}fY1jk5BxF_Sq30_LxqrM7>twGHQLFLKBWwdRR`;nhc;lU3_;5l|*Xc1F%*$pz`!jNKM>ib7ElU7VQ4(AJP5J*A3w+ z=BKx~iVqaXtUW9Q($!WR)lxQEbhgnFbnP7wDL5>6ilULwya&d_CLkXyMNI4t#jWZ+kPWo&ZHX-brmS2crly&t z>E487{&KW;jODa1AqGgm>os3E1tzMdL00kn)XLi6j|K;h`jbsfP+Uedy&xI&lRjy zMaA85R`CFst+^v-jl+)7L*b}2xszb6 zIV_!Ov{AK45~jJ`Zm}lSER{(-5n0jP-`m@Z=DmB-88!&tVYQ-A3h`!3I3Ss3Yg^IS z|BUUW?zSL-hlTe1-2Xj}=l}8~jym%MoPWi~;V3qUnq=Po{FB&o-|yh>Xobb;f~BZ9 zt$C?K%tWw?Hf=^&49n&J#W4T;TFML8q!&SFS2w0lYtRemn4jW-m}y)?BO81!w4YE2 zLHafAxm{vRQNw)N;|WLsqQqg%xWqWKW8D3H*w@x+l;#!NEd)kS$Im|+7^N<^6$Qp> zp5T2zvZR)7P|YZA7eV47q0mj4I~`|y;v6hLZKZsF=ZIMKZ`Kiw z^TDU_EwLEgamO8)KYzaLG{rzTnpAJP=_Z_X(n&IEdfmEpxaz8_bbW%HJ)cI&S5{ZE zTgO9Y`C{@H;*7*n`5WVl50ekd*}fj87IgFtOobJ|-zi}(x zf95K^f{q1@Chy)NRs~yr&5Nl&&PuUbr{`87VKWGdX;AFA_WbkD%LsQCDmm@6)8xX( zR;Mfj8(;dDf`E#GR_RUIj1*Ui%s1r?18}Qsq|A&sYeXz*$W{I7r^m6MN05|o6pOk~>-?9$P5Bv$e>)v2$ z1S}K|!r~bh!e+U{ipka6uz(5OAs)1M*KdNy>w~Mv35^WV{+7_t_lr9r-vSK7Ms&M9 zJwos9?#9NA8)fkr`b1f{WY(-%@^63o)1PE26v=*g9e){l8;d&TLY!(n@l+RFnsd3m z9c?}6^M*0Ax)`-3PAP!I*LTl`^(vRO+*KObNW{ScA>NQ7SkN3UKbwY-krpYLV!hVI zcih2<5fqig+N1=LWaozF*I*=U`2K}<9uTDFOrc|1ytTk4v`uQ1qY{spLpC*ZUUv$l zV=XRIGp!zTmoLOyFT5VtCUtCDx;adZ*pDSAuE1HBT!7l?_3#Hgc=L_tgcg3in740` zxNUYvR`1u+h0Cz`+M5g|&@+JjkKd22kNgF$FdM;G6PAzFggy}y7mM!R=Kc7`GcVwS z=e-{qnLKSm9cg+rKmF&NTiGllST7AQJ?Y0k{_!F2fBoxUpOC{)ZB*O(JowA2o1`h1 z@4c{T52ja_;lnEzNc!=4J+dguq`0l3Mo}5F>{o8itrkR0SO&(K(eDdN%`5c~tV1<) zZ%}BELKvxXQOh=RySS?|5IwFS>U`)Bm@w1|7dI(XdaYT+5*Dq=sb)6T=0AGuWi-uvzw zX#U&(qHojNFk7rJ+3W@X>=kr*HP!=u@NZAxjMGj=X-P3O(nRxfKAchN>|dXNwZfKC1<9b_=oYwGXyNbu)Ol!_GwqPi)YmuQ-19EQ zLl6809=Fl?aJ;&G(sIZBF`h=DeEG3*$ocnnqveVF(fZGa5gO=+#aUF4+IvA0y47mI z{+3qU{;NCj?W@162|6A4I)P1hG9)w)86hlG4ncOcM;7eqCBeM8rxR=UcA=rX2rH)7 z;+*BPg~oI6k?ZXD;FZn0QDQY=<;)uF>+#^uS9f4ReHl)fQ#YixeW8JQ;Z{{pWE>8n z$Yw#0Hz+Lwl`i{WXSZ}f;*x~U9CV@aTiN6z7?MPpjVf%3NSFdcI{nj=&OKHQk8{mN z!hqX5k|j{F^4~R0(tKPXK=pU~%N%{OI3mSSBuR=KbnfDC8Hv z_ysPxCg%!fUU+CKu)-jyOUF%H@*GUukN`hL#@8uPZ}kksa-QZnX8fW$VaD z#EAZ74s|@HRk{TEyRf695C6MnC*HTP0gV+d*@m=GAS2v^VVSljOTLcNe(9?ZhW!{2 zgkI%xWMmbfRfJ}HYs^(3ucf$ONV+6S)|bzwMoA)P{Di7!KUQ#lU0q#S(b7qRmt6lP z-1?;-ptG$5rLGbgB*?(Kg~u(!8J|2?R(8u2_4QNnxqrV_%r%5pUj7Hxz4M~jHe0Q_ z#*~J~V`l|GT0SV)WZE0P^5_%gAOhW;*#GGNp>6f!GK$_>R3u|WTv|iV4P34w{Q8c& zW&Ppv&N)*P_GozHNBL`N=R(Yg`{`$8yoINqep=S(o;Ppa;KHzW?b<QYK0i( zGBP(MQi`i#o(rf9fMkJU)N4>*imV<)Hy{Nv9$hHb{Y{a&>hhNK+^|cW&ksMNr=vy{y3qpal4o3YX)MNO=l~By^f{nSz+DYoN4h zwvNq)R!vaoppZZTAzKY#_zbb35$iY5nvkvD>zSOdiGZw3Dvq7!He2U3C&Kn*<1K0? zYu{XlKi~I=CUJ$`PXbSV!U%-Rw*oRlv~v$I<7}XEQC2p`g;xbL@#m5yOQab-7a)0& zc-w@AKDELnHPx}M`6P9RAgLn+sZ-;<@6Fxla{Fb&lf0#KvEKIkL&iC=j^8}FC#TS= z85GAZZ{{=?e(5Uklh7k@CYZOf;LyTDQqLx_aJ$?dtpM2qg#()2srhGa%sVxW|9GesTc%TS>!-@kv(f^XR49J!n$&_ z^m*{`yL+*0Y6Xs;RV)38(P5fu{Fo?hrr&48Akb1VUUB-|A0yq{27I#psmq>z9rj*t zP&(5cN!AKl{7AkeUY=iSs@Wnn3ZK-fQ}Mf}dOjEs#%)9Ad-rasQK;x-n%rr@S+r9#EPkvFbm==E-sPBMlxe?Sh2$< zQ;Yh=f|M)}EbF97BTK9gI)l`tmOPG1oKp}nkG)S2bB*lGmHN(QijyZG%gEZT$psNE z>~!PMPfaZ~-U`HXY|SusK~)nZNoE?EooJiNOit3xcIusqLE)Tv{`k9T5>#Ge<^jo^ zR*vBnB{iXE84S+g(sAL3&cSCt^D)E3zHYlDZEEWL-7zd5(k96Lt@S(c`rBKva>W9i za@-^(c5gfI=1oB7yW+|g8)RN60tm^xB0J`AO4Yym)vx5wFMQz(aMF9}lkCDt+u+Q~kj(&OBl!md~h@{#iaZIbUqW<^!QH+`*9SRK~fEr%{%gHIJ}M z5cAwL50!Q2dwl`eW4`#2E}=5NtJ>)lm}rva^=S^f(PoOGQjl_@Y>JUU?VQ^sLBq+5gW8KnL!5=s9$B^+^I(m6wv=*pOQLI~z&tTQFm49p3+*qfu7sLcwG2KY!?hOB%}Brb;!A<)XJln_Y`nt!^ACLB19<7Bm+;0LZ(!-trSc)=B10{O3l52W zIH4=TONaPKpxfK~W$yKi%HqLV>Nz!N#ClBASML&>-6Hk;Qn;>cK=;)W_LKo!7cRo59Q7ew%?WO~=hjo0OI|;v3 z5Zn=s6mmQD@H+OrE`7^=E?fT^p1WBxPeyJDm&34QhI)mXD;4OXpM zC9fCFmMja~($azr8#c(u_xOcTnf&-u`Q4J4P?^@Wy{%uGY1x2D9qNdnB z7-^rVISX_6+j>2c{9QJS+<&Ho&KL%Nz>k36C@hq%A~ABFVml;V-p90lWYe!cXmyJD z*Hzn5VzT6xYeLsZzNm#Gm^q~yHC)X1tv1ZugRV|Ggdp4TAOa`b%HTh_j(}bQO=FXaG7|i!RgGpEtKRa4^{c*s`?~H5xc-CsC zhEA~Wxxx)M+%Wi>Og{EY|JrlPz8u^&ZZGZJZ>~ z?+?mIdZ%^bmK83tF2=Z6&Dh`T#gZxIBa%O@3hn(q8BE6pFNIK_oSHxV>D1O$g}GcI zz8DQ7;~Akok-qkcBynaPDC-E%e{*xQCiH?=I>iHDEVQyYG`G9=0-YPgS#;2&i(78BXf0#ukY%R0cMTmMdKdDolVpTE!{7)>8)-3GMgkDnt!+37psLn(VDJF z3MnjqyT8{Xi96Y%y{FqFVG3 zpkPGT0*`@~51vQ58F-8omUMHLNeY@47tWtaY6lLyT6opja)ICgFzpQB=x>S>G_jI8 z7wIRTd=i^CZ(Bz9_)s=TXpoLnEu3>_b+N7Fn9iBaW+XER7}Ov{xJI5;=PB22cT zGn&xTlHww$sw~Cy#u}h)4d6Z??&ibz6q3lNoCq9yP2LDLT+rw{XB0aLeBHWra&dd? zvBz-r)mKYN{N|f)#^*o(`60zRRCK$Fc8`y8Cwafq+=aKByJh}%nIMah*iNQoGf`3Q z!%4Wh*pBV(1CrF2PAN+%IF9G*9S8b- z*x%#E)bb+v{Br&>s82{8JKFo<4}~$UqG(Lf?FR*=2m-+Xg5v(+wn${-O<>CBt%yAr zX>O??QSlg_V@0GSdfr18S#QKF$zBDb9#SJejeAE8?t|A_SuD68mFLd$F;V4bHKFGb zxQNf2HB~GY;+Op|8x`GPJN!-riJWO@vfXKZ6@p-(%9br#aQ^w{<5Qpd6mGliHmqE^ za?lLO1%BjXIi-FhU!3*0DU|U-s=G2tR2yqf$re^Aij&hF&V*u!1 z-qA6Dh4tmKAk1i_Hdt0RZu_)e^!PkRK$@M4GXI`AIcDYDnCMn;pu_FQrnY`euPlKz zR!Pq$h=Uddrtg(HZSp)sk$R>=@^d3JWt#)GJSdK$h@JO3P4W2W$e!?i$7-Vr)4P9P8z7bL%kk~bBr*3FwE7W+`Y(W2CR7|BBv zURWGv?uEeOOY%0~e!+qT(rotBQ%^|~;8j;$C8crxeD>LAzUfP*RAjY7UcUL^dGE*PmaD$y`RWrk z3V3NfbJ7qx%c_3uvJ3Fi>+A8xyW3?u#C*88>Lexq(WR0ux;!-aqT{|auM45J$PvCPIS4w zm{MMh)0fP|i3?|7?T&ru_xQ6C7?{t9&n>4pn!#(zZ>*sSJ-4PAar2#e{4zP(JA3i` zEAL{@{&rascEYL_iB)ditVScAz4d1C#;w%U=^BiEwrH4h)9ETva&!4^=J6d~i zpl1M6DoQX{kb%b^#Jjr=U^1@vGQ~>>%QQ_hIIN}Di|K;Y)9J>>9_Sttgf}3A%UI(+ zUxLfHR?M4HgMMEC_r0>&FsY0G_ICB-FRyOGXICx9+$q&~Yv(>3PMIn+N2|zqRX9eI zyC(E;0Xv+1!v*i@WN~B(AnKTuH+t(x#vX(bI&{8oqW;lByo;H^1!Y)rHLnB~@!alg?ZlGl^|)Z=0z9{I4|;_*URUPAdza3Xb8l+x zl0wPhiul4Yj4vU%PuARWP3U=s*)yidLF^S8`^#@^#D4L>TFjQg_NTc(I26Y8*)=F} ziW_{>vqlv(+a!=?!#*R;Z0c#iqKn1TU03jj!~mdQ{Nfk5_S$P@ZtQi}U59Uf``dWn zfd^#t`?P7(Mhy<~`24aBD2t>__NAW6G=tjcOK%n$I`g)tRixgDjl%AMey{Y$yYj?; zeo)vjqrK0A=hyEzaHQC z#y19wV#I57$Z=X(Ufvescp1IlG2q9#{XLjcR%D!qBwFuaB)!Z(522;cD@&)c+-km^ zI+O7LqqE;52Q4&eZLs9P>N4c~6fgl&JCA=pnDIgFo&I-f@O-u=YwFh_&xg2YXx*a) zgXe|EPOC;fTJ0u9IAM{~ffJ5dB!|?nynbxhvKyOs>_ul+pM0v>Up;Zm**lCmmB6Yug+ZV%3hL$yU# z72{ZJXR_h2&fupRzOlYqjzpm8-scaXvO-+=-W@>2Jiyxl__~BN;4^~b7#S{aIMcyZ z(Rh)VoRwA`|2uv19A-P~Y$xm{nbZ9qoN6 zZ?@yu=~Xg<{g8!Q+~=&G#~k!oRl1*nMrv9;2~`)4(S$xBpy{4NqI@oJ?6vx&n`qM2ItD)@( zt*3LqhxPl7HnPPIo7T=XG&G@WXk-^OrCoE)HE3&V!#(%hgZlbPoSGUpE@6%CV@i8vDEZ zv7xzB7U4O1Rz1FQ)-l+$w-x)l`XtF`MWBd8a(aK{a{Ju2;Xk&?N`v&t>QpZcP3RgL z8G=N8)ly=7D!&(pt6fFMDG2X}W_9D=jBy99T4+u#Iuf)gybv$zBe?(Po3-Q~Z@ z_jjN3yn5>G&Z+78RCUj6PxsZcvzv3e4v>mI^E>kis#?FwzB|B7#<)nEfNZ?oyWZaC z+W5Q<)$$lk&FE63_QgmH8?FuHX+kFiVRQZEB5_{EPnd+57%WT#m#CXH9R&A@igBkz zf#$py{GhS|&p%yM{?Yr(LkSI)e24BM>mhvWaYI3fi1rgFzL%-Rw)8({mR{hf_U-9v zB&GtZPDdq#_Wk#>k*}b~sa~e<<=`V_A)?N#dOp7V_J4PEMoszZ2YG?pwUIb@&I)j3 zG;B5M}X!bx7w!|uCrI~=78TInAwXI{{DUD?KNM03hc_FFW(Dvy5b zd!I^jvFn4C|HdRa_gS>6ygaFDQ7=H}uKjL}0e9su`6uYBY_Zj!>7w`|9OQrKpfI9^ zyG${ou}ofPONhy79I#756#PFCA%rW=%XdpVkdzQoCr+9R!JjHM>+hH^J3kG)+=g`w zj6S=xyy)5Se;}Hz{L2C}A^#Y~Wx|HDOG{5=KBCJ>)U1^e4M+SA3)`%7jk~T#-Q-hI z57_`i`pwDHvrv9x{cOt338q068fuuJh1<2C&{u^q!LDDj2D$)CvISac%(j1!9;aX# zP1Q&tT;q=Kk$tJl7CM+fHeA-<4H;!{l+0;U-&K2XFa)&2x)sYr58Gp_dH#r>h zE_eKod{(a{LMdl8Y*y*ajQcElqUIjw$n?JTMbA(dC0aR0(eDE%XaD#5ztR3Bdi`gj z^N<~{5QEg?e+CKfKl@uW{|tg<`)7X{O#kd(<^AUdO-xEEtUN6fQOhLFt(8NB7AtW6 zX!M|!@$S!sC%cgODfhI=-US?VKvc(Y`i4mwcW0S(!GHG_B?%0-b!ScSQ=IWYx=Do; zGbblC@BdH31gTj48K$9F|1~DM2it2A&F^fjJr#0BZQPw&aP#d&OSH0=V()O{8Bt$mI9GL5kwxgPl#Cs9Bzj*Vrwk)^!Sue9SYx9 zun^<*YyrAU>*~Q{FBZ;nD~)`ia=&;joJXEf?;IMJr(Y|{w@t4A?|Uo{4}w_m5(-=abvGD32%G`#&oPX_vv9tz1IyO z)0yxCg1w{sq})_oQ6Vp**UbDvA41Sfv5j{U6SQmPqWy@S!b*kO>&|pbwr5$FKj5x} zH(6--H(CAgUfPdbn3t0f_YnKPexy7xz@ONP`fYk*KE2mV2a`z zU1e)7vZ)3jXu{Ox(|R>_ICeK3XkXEeYZ~GqyK;~;ypv0_&`@=C?7a4856p-peMP#} zpINmI2O0M7w#Pp8XVfb!!m%^MtJZJcd!!;BussW#sK$*f-}JN5`TEX;E;_g|k}o3K z+Pz{^bGwP;Er|cE(GPN*mQ2udTm25j;r{1=vLcu zn3mv9|DH|OY)y@@zsFNw-6j&Sc5e|^ z2F!Suae(TSt~#>8vxDMPF;cT3n`pLkCf@VSyLI>TZ=7ImK|+c*R&-Q^PoELiMC-WK zM}o}`YpNOf;P=vKx_3&aa5W(9B6XFtMWeaWBhQ)v#|!aeOcq_fsNH6b-C>T>tiHrH zth=ty<9#9uU|l=D*>+<~hYf#%by^;wJk2f8hb$PsVhG`@DNn5ka*4TZB&~l?W+VP6 zPJ-ckhU*-Jy|>gCxHBH)_7K9VK03$kAKGt9;i~_rzU>HQM|F1naA8}7O8^g@&~{>5 ziD@F@+%U|(2uqQUWh;8YeTh%8D*vs`nIMp5HPCW^VaK0bYcKoVk<<4s_I)+KL3 za1IHi#quW*^pinZBOpzjVk-!XydKM`oKdFd*(Z81WK9V+wxQ)hv{4f-hu0X^UNznt zABg4UB3_%yF-+FwPkfxCrP#ewkJ7VTaV3F61U!Pub`8IYW_|> zbqQ$e$Y#G-EoKX{y?w2`Iz*cjmr>BSeS|m|jRi&tdQ__ot zi%X&rFvmx$AVz`0%JQ7;=XWnvr1{uef@JC9EmUqAih=IANrUjb-CE5kL~@I^2B)gr zEX3^v4OyD7QE#8{4cK69ibQz^q3n`9nPM`56+fV)X<2gR#%|`>Snd!!hTwpfPYtv; z3I~k&+VZEabZoeRi30h%g2LG_vfIklhWEOuW%~y%9AiHk(_mM&pl?v&IQAeI#qQ+rRkzR_N_O)|(7Ue$#YvraR$ZhtYYY zjKBM$`gMZTIg;bPdi@W9l;*% z@!UDugw_u#8XzC}18vdTsj41C(p8n8i;O|(NEk!D3e?DNI^~bn_9RigSR_xfSRpZ= zDI*LKqkZLb46V1*2avFB*;S(sJ`=Qc3cTCv&)>Y?X=lZrKU7T&0jDo9SCmHl+J0FK zF`8f_+GBk*%_tuF8{)Xbr>4(P&blS~;omeIbK-8+m@p1(QHpMSi&y|NFzIf%Rgeqw&@P0Irs z3}NXH>>bF(GbRY(j|$5c#5m7Spf{N~kZ5>#o4vS}DG2i;YppUqwZjuX!3DCceb_II z{iO!jtrjHep_oX{(SAth3Z>7ALK%=6Hd$LlQ8VK}iN5%(OB%(%UBA6+zfNfzn#b1i zs2*JW1l1<28?litObiICs2_nqgdY-v$E45G)A6;N%-D-OwSfF~b;1(Lr zK9%@#FBckaH;#5vAw^6V8oI!{^F0Mkj6@b_RN@BpXz9ZfQXvR{xVn!>g0H60z9rXG ztERODpr^M!Ds4LFHH=WrBkaO!(3w8q1XQviB4&vz85({acxt&WV}41Of7U>MKnk1y zSLvCY^ETj8)P7}ZYa}*H-6-3~*>NggUyk`&zK&8f8t9K+X)y;`2=tp7Pla7w4D+#x z6x$i{)Wk9(^Lloa!fj{{N+0104P??C)kw9-{AM|!S&ed$qEIHnO4v0Avs%pTWZXBx zxAHV4S%2fh+E@wwcOJ67RK>3yLf?VFt5=NUpI#g6>)}LeZOi(NVx;j9nv6=*-!B6V zLvB*X2RH#$ooE$)qvxl7*f+CCO2?)2jIu$4!Hd|ajA`shsJVn(;JduvM`6SE5}btH zoq#hDpUz1XN01*?3>K_#VVl-E8f4eodDNe&Dtumlsp7qPha*ubQuc)@wQW}M0h(~&Qu0QpzYQ7G`wf9V0R8!$UykA}wI0pNWD-qqg#dgh<;qeSKUTk;2d9R)EuJ>1W* z?i^vwsBYdn4IqPLcNzrj?O|eYmhRWr*8;d&m)SaVKY;sCIa8L{sw(yRx30Z?JrA>} zbGv*ttJ7aaP~!z{_AiTu*2GjhMd zb~VYh(I;lpVC3P$5|gUIQ18O=2oLuKTNoFhVG{1CLB3TGzmbClyS{iP62BTM5j{Ia zW4Jlw7s8Xk?Xulm*~_hLl!DG*jBRW+*Ap}GDLL!g2xc-Sk%5kDIhC5!^pq6CI$z3E z6{OegPE|eI23`T?Oo-)M$9_*$eO*|Nlt=cTHWdm7pMTp#VyO(eX&Mfu2EE^)4HZ5R z@i{Z_%rJ~yjfMQI_Up_QQKfM?Vx#)|&PfUBJN9bNdW*`^0)#ivqqFVWIu(Ks4Sn#> z4o_n_{z2jSG%k7%LkI>Bv-XfjF#32S9Dt_b@#xC= zR4~*1@|g#AIP>n8`ysbcwqNZ`_tPWVk17`i(RfcL0scsS6eYW?nZHCo~aO;3s0 z)^9YelhwsFGBFn1r~Q$->1<^`k?X7(DAURgA%B7OD;V8}M85Ld=qpXX9ogtkP<-pC zABdq6rIilZP?!H`pJOA0=s6Ejw1%rF-$m4%oX;)mOo#|n2fI&IPgn4ouMaALN-6r`gZ@JE5zsLZ-NuLVZ zvwf`MEoMtp(-w$$%Kd4`BxLb%UyyP@@uP3mZdHl^P}pF@WVMj)snj!0_4n zUeS8BqM|~DVNdVgFTvWwqmgs?iX-A%m>~=5n5sSlD0E$OALtj_nfRQjjTLP}lJazP zaw1`6Rg#IhJ{>FsZ7BqtC8@}bN3iviVJ})2S(kW^ERv0YDDU&=R9*3i!+53eUFY7v z6{SM375(_UZ-Bu^=l-C;K6HLPwj%&1={sPLC^xpyGiunrhelB+&`lwT1!UDzz6s(S&wFG*mhkf7z5AcnNA?gm-pFUV9nf-%u^QG=3Hq*1r3lh+)7f6% zE{nDnCF#RTwEm-sja;N4L?wOGERHdE4(TNyP)brj>gHLdKdZ&piERF8`eY}AP~c%| z`DVpaKz@0vLHgNo05EPQ4smjaqxITcz7WjQpeA%5*B0n9zs!MUr3ZYDXzeGX3UYXR zht*nY1m2N)QUW)a^_cO3OR)^S*C(~=>Lk)PkA@>^esm4=?Jvoa@8b9SU2x5lV6^I( zs6o!s)}n~eSLTe%EGbtGx`RFMHfSWhub$XA(c&v4gm4fxHu!LWgoNcIm(kJDLBY%G z>%yil3T}3)IjTB*jk$71m0QNgFK;LMh>n>SUZA@a4Tg%_39I20Dt?9InJ%XQ%EY9k ze0e_uLJ1c+GdK4*XUK!pgN+nTtbm*j9D=R@&Jd7{tX@JoQcZ6&MM069@jxDhU z$Z>Kq3>EcvlV=aFZ@O2>Cs^b1#snhxVsy;eHA*=^hG>GQojA!cl*V;!q7>jG!8MlDwT>+w{uT~_((HS z-WOiI9z+yN_2=Oa6(FXB0vlBmFq~qT?6#S*+DUe1pqKvh&jeoI{+PqP!9Co8E1Lr) zEswAa=6RKb;R6J!Z}8iP_1ciX0BMG-p~*J%aJ*94IHO5OT?( ze8^W>CzNeudj( zKMisCCc~|3NryqIIie^Za|qxS!IBS&;m#z51+KeoE2Qg2OxCe#ZcxC#;jBqaW_A;pCH^`gU*5Dunq)=;*jtEMjc6F0T@HWK39k0-}4_1OuR> z7t`ifyYvre@SNB}UxB4($FEjdra}X@))${jQ-{+?z0woG8ubK)T+avT`$yp@8`xKrty>|`#B^8U*%i?A0n-&>O{T^-&Q?~`G%upN*j_}>b z5r)+hzn*?Q4ceOwoya7P0Benu$qfiTLe1JY7ychtDBvy*VjQete(@K&9BR@1$s;H=DF+54DdKdR(cnxf6W@iHWusVZN5 z+8^A#<|^ldXI=kci;M)HsO7)fmLGKM-Y^k6COe;Yn%<4H!4$N=&QEcQM>kpbMW!e zA0dAq#r2NPU20S|!)!k6NWE*i;C0S_S12%-N|*iQVa-yal+x zP=ns$R}k;TwWJtcYm6tNU+Gk(FMHTP2$76E7@Eyf97&NB{D2iRq8>6KqS9w;z>U%M zMQN>6`ZIJgR~l#gi~6#atB{$VMFnpdG@oJjbK#?)bX9dWYU*1g#vnJR42-?b*{5?qt+evmF-FmW;Tp<32Tw0iK1xQ~7V5O*dcfEU(x z_+LF?_A=*7#bGyVNnMiKs||g4)jef6JT6cYPUfBly|+mQS}8eNSF&M)KP}JWe~L2)&$)j!xDW$oB+hinwJ)7Dm65h zw$HTR-V|S8^+g%xRyzsfarF~+AAOGy zhF1JAI$Xk?-mr5mRacNDEq8Cs-O^-#O5dg z$z<55iYOG|(EEk9t*eq={=#6kb(|&DP_n3DqkNB*1x1S@+=I?>dDX}s(V*8kvt5(I zQg`e%O=AV^MtZ6a11@_Y&0sciqUy>TbaZNXX+_}o-g^$Vn7QF*EeBv1Pj%yg@HX;O zDjo@LS-rVyC$G6e-0))QM7r0=UYmj}oWjEX=SYT-m}-|%ZnsU3ak8>j z)^IPYtXC(lUZ>LjP?{>81@@c)9T3^*p2z?fWo<@=#US-3MbEoKH~v456$Z5seytb* zvS9>OlhK&U6|oTa9%j81mn6bm?U}uN-$1}g!EWnwDl!S?Hqzs`TbOs{W!Jl}(j>n| zmCgc#Z)0O)G2#4KNH)DPlbYwMB%%8e1Iy1uX&Y~BYUbgh=BoNJ7wAt7qi@P4cXEp> z7-qlUFtp;9qf`&Ptlg8J*to=~}E^8Cc~XKkyl9 z`YC&>KvMzt3|@Y@eiZa`^>&U2xo+hlkKcLfx&H0|@qwH&$wiS< zRYtf0y~rZO+uP@R*h-~+x?$!Q6&5qH@Ls0evC)(vxk6fXRl(k2BDc>T9C1xftqQXy z40(W36YO{Dn~o-T+~AX@o}+@w29VW7#7~+csh>N>D|!VlbO!KOoYuV_xA(N@G1DBv zf?|Nh9}Hq1L<@Af9Wioa6LWjWV6Dn@ggDC=5jN1m!Tz>6+ni{4m%)pLZRJ;usJp&x zr?MrAeqsTzX8}EYI7j?EE?g#{*7>v9s3nHViy`&*gBBck{pAjMbMSA>%}_^I1`pc- zYa5I!iH9PU*;(7dlja*JpyTxP^x^~{FOAfkvqq&VmZ?dvmbqbL@_n6c&g0etuw(-A z+&4?ql9wQkln9QH+~CbK{s{H;*1R=3?}G9Myl823jKQl4T=FYiB-}bw48H(QS@&|I z0Jdi%SnzzP0WW&qWqA8L5iUld_D7tPqaDa6mNHoKcqTJA4V>D2Lv7V!*e35Ke!jh( zN$t^Pk{@24%nW9tSvv7aAv6^lANsZfsz{|rY*?iOOep1493#*95D1vGX0v(ijHNV3Ck)V&T3P-gM_q!=6lr;l1j zI47c{16FW_Y5MgaOQPUc8y5$BIq0s9Jrzix0P`f++Xh9vcA(vBx)X5>8Ye}?SY$Ws zT2Cf(C+m99=v^a&KB46Dg73H=IK+;R0HiUVb>>(0gp^x+W0O4|Zr{Yy^W9;I^ve`&F;eD3GpSLEH~ZjnmgF=wWM-%)zjWyenp3iQ)*vrD`tq%-%;9 z=d-C*TG!9slE3QP8dXDJmcVz;HqLn2FZxdJ-eUNvWP;F^8wU# z;RYWh|15ptZ=YNjiC=(b3bja`LDHLd&qyT1Ryt7XYl?9!qqna=_Lc?UB8cnD^vTiv zB7|oTOCLX}tL%PllAt9PX0Vy@{dK#`kcfm8L1`MsS;4RKbAxS^a%)D0tyWyDxl{5I zQzkiQvo03VkLkWr2+B1wplehRf?nSg*C+_7#>#)>*|5*69HdH@Eik(UnAm+zSmD?v zTRN##&9Q**+lri8OX4TnOHfSLNpHs<KZn2cCP(Wfp#T=8I&c~~`D(g)ohlHl6TxibR%+Ei@ z6y@RQ(xsI(bva79*@MSV1`)vG7Q2=hk&;;>zi*n=N9*9zM$#sRzY1bPu+Z%V5NxtQykAgvA1rku3h3=6cN>T`%@|%f6}ON!odwZAsiiAoVI;>1TC{@fFORr_dUyCc}+InQ`)!aDx)I_ z<#5H^Ev^;ZN?#Rw^zjOzQl7GMbKiNlq`2IJkyv9X!!Vl@H@6^_fjI{Lfa^rd;CCd1$ z&JPKlOOH20E+rwNWJmMH+I94@8ZRiK)}@lz!KY`Z&0DAvvyfP}OsYs zXC;STkm-f?D=c>(`S^{vIHZ%T^#I=Znb(S%C5k;j>~t62di``}UyAiasuw}DnVd7^ zGA@xuacCzoS*8t-Dtl&2`puv!(sm1eO~H@^eFx#sT(xOK{CaEt&eFCdm5YTcKUl-^ ztPP9Q3q?G#&z<+u<44mhu~&zRx-A1L3d&#rlT_A7(4*e|#C`|=->{QMrzkL#xMr?BzPj zHTcTQeP)GUYzM`f$mrgga@2^0h#Z!l2<8eK;ki2mNhSV>r+nOs+ArpbeAe;oToN{H zr3}gS@xkrIf=5t*wBIne$LQ~8o1H8KFEm@%VW>5FTza*l(4IgZb0hg7ju+jv^qCvAmR0}I~wNrhzT-;AKF zJ&bxom?RozO+_v6{E@a?9--_pwh^72)=X{TBPTR@+cODJzP60Yqvd23PHL%`+jGpp z%_`kwKnC^!E}_Fql&OJUr>R;R{I@1@@=dq9K}Vm_ubKl6ld>zHkCo=jMoqfhHwFfz zJLDcAZ@1N;;V+^VNeFtZ++`u$N&HxCO~2A-C8&MM`HzznSk9_q67Z9ED|K-9>ax_c zqCO}Xhu(&j(m0)-xx8gP=^Wl5buAEd5c&f@ECmXA0zM-R)?de&m!m8w=EM}imW)rs zKP?aLFDgt2kcr?#pOAl7C@QW1XRK7T-(ag-Stq~%T}WMLl5t@~_YcI=T=b<}-@npw zeQ8yuc9rDvNBW}*3IDeQ^*86Yo$II(%JzyL@kn4^9IrgJYZM3i&&kKxCKb)O_&$xt z`}W5S-xP538$hV2CfS&4vmxzIm%f3>Vciy<*@A?jfyo}}XoMAc1QheP9N}ILQJI-Gi(3ii4Jw#bRL?UULuveQe zQ@AE^)E+#=K(eNsBLn`ds;XivWV}I$9Ti18xdaxD6?BXhhw*r>6Bs7VRllr(tWYwf z`|-!{HR7AsRo@i6w6rL!P<1^pF)^Fz3Bzp+sVXX55);aMdqQRFNR^|$ zBZX0ahsb`ASKka=mCIQ_N%T29pzrrNw1L@TLL8b{M;V_EGLE15Lr_pdr$)&&2IA{J zKCx@xak`tm+5dG**nQxo#?!$%6Y}Oie$d|n_!RwK?}yJ#8g^eTYZGuQP?C2ALY$~; zkL9MA;CQL>c(U?{%#vN2d&d~cERlF!G5NlBQDW=eqq>j>_e8ap?}|&-y_r-bH7QNoH>ogDV}4c*q*>+`+iIRrwYU5=yJAl z{Mf!aM29BaNLZ~-Y84&lO!hq5hA;yZ8s6Y#VYY+ZGeyR!I!xJ}o%^Ts30uJA<|2ar zQSCHSHu0i0m&)(CYWjIon|YyH?Tn*6ziqyt51K@zDE?=he93~t77Unt`4{Kqn_H)3 zcH_qeBFB4I`{Tp>Mjs>FJt)U_bUI5J-6am)!~D{2%Ib^2i-QYQdHBKl^q|INi6@jkB_YUWLq8!+9C17vVQXgAEhrz%t#Y*|(uM^Hzd&Qy5gax=1t9C4$pr;Ahv!{KhD zTk&#j`?|B|asbdAyE>&vQoBw+ZY#5i*sAWJer~_JyK_htk*L_j7ZG1?>IlkiLN723 z-coU6mw`u}SK(vQt$5v4U~%(=w={mlF3o9ot#DEUDc_#7&Spe1TdJ#spQu~WMW z!6N&S-^U0+nU3e4UK8i8$)Bfd9uh5Hl}^g`=)sE0gIs@6oXB^&lBH77OIlS&ZhLss ziL8?t7PR`T?AV{=Zggvn&KH?fwMoQzhIwepOJe=#HmZrRtID*wvC8TVdpI^>&E?R5 zrj7`D{M`0zz$U~n8@y=bcs1y2ik zkVu%2X^&|{KK-$*4iwrGd=)fpLVvcl-7+9W^@bR!6T?=D1Vmiyk72`?%Ny9QS9p1--=@Tv;QBtsKofqcu zR1&f^g^#nN&2XqW{6>1|10TYEH`nVUTEt%LxKO_b)yyny$^Y9z<7Des0F2y$+yM%V zxaS%}znJlTbmzgz$tWO2-Fz(;@0O6qv_P-H&Fd#?IB1^G>r^+Dj>_NScA|uUNSpr4fP!V7ay)Pa0{z zqqzzOZ{R%p9&F856Hk%Tx;kT)7-@R3&d$Q*(__b}krniilu{Fv$O9-G%#K^4z=5)& zfyrECdcu!ogg;OT_n22`G7+;7vy`Vl@2$W~FF)zd<$W-7s?IH!DpR7JaEJ{#kP7%F z2e-Y`?CY)7Lj&Jl-(!_K8NQt)ElJ;ygZiDjbrlFYgW|PiQrD6;E%+d%t&JMz6t1GY zQ0@#Nle$6%msMk7EBy?6?zQk*p5i<2>&!PNR(i~XiN4XUkq%@;wyJBM^6LpMA$F>s z7A{*aAeL-6K=Zq+XPGOzLA-l@7|_PmIcfaEKVKAi+BrK)-rGajQKA>!4#e%5NAh4{ z3biMsIvwd`!7VQFJ}0lNqO?~}T>>UX1*I>iQ*dWRHWl6(gcUni*O~$*MD6{`1lb&6 zc}ma9ei!(gb^d?juUT|V#8W(I9G>AvARctP3G~XH%fZ=s?Q~JLsxV&P86qlpUF!g} zj4sQm2GrGKrRnd3T1YSCAcxxHdM!V}rfD*>z+@kgo(s@FQ?Ut^ud}C4q)mK6CSula z7XHEDg=jhywBjdZxau3%rWP*If;Fr-$HKjuMl!MWPY7I?liA^7Fok1VH>jW z5JybAsLHxpvoT}-1`K!jh%%bP>DXSEWEsfx;#m4Vx=sq(j{r%XS}W3Y^!0bFFEklt zJEU7D^B>8tSPc`9|4Q8d7+~vVI!fUqbR>+#uM^k$d%Gk9EwJqLz&GkuwfK%h7O@#E z_S`pLqgSGL-^V2^5%sn~#qpJWlqt&mZdzom7^_i-aL4(vqyiktXs+A0Mz9ne*qb}-S>y9%6A<_YqR`Nm>D^+CfCNSGClFZt_F2~3Z0^VU6J0P z**O$xHu76q@0BCBHn#9Fw4gDsOHqn`XVxDo>Q?+ipZOHb@z0q$tjhML7W{sTZ7g)X z>-*+>B3nV|P=M0;Z&5Zr2K%BdIvKP2>{Cm6RU7fxMM}E8q$Ht=rV4X>*QJ9{0KEQw zt#Z+-PBJYle%9xZ9+p-AB)qS(>0-;shNAz3k+g$0xQdNrvuvB*bp{4j-MLqjj@DQv zFiRx3@|H$J=D0-%>(#nHWnFh=QG1oezgah)n7Mht4cZMb&%ajK$HQ!3u}tk&PxzCb zoXBe|k=ehv%g$cGMAQ=yVQf)tB|njzeAvk^xIq~|k2n5zAppMN&(16Ac9y)n z{8Yl+iTl;ZQZg(n?nnKX++GIquP8sJ{7bVeI*qJ~OK74(6GfMED# zmeUGOJWTqk%82z}R-ytIfaxWpy3rm0QT_!TNl1qtk2kOJj)}zx`rGlkv=~T7l`-yk zhpga{oWs1s1er{3{440yc%%j3SzQC-(Pf4cd<*_51NnFY#gS3t+SDAps#o zG~w43C!bdUC(W-!-=zOtRrroz>F<0qdwtM*r7P5%G18LmNe%(U*2%w9zgF&L$Z(i^ zn0!IX9G@xDs=}RQE4`&w`A6&kFkcR#p~v&P4Y9U`rtq^_|L0F1D#aBtdAY5&fAU3E z1QG!)%5&f=M^IPrSn$}F89~Tv@=;O|5#tQM{Sl(2BQ&x5Ryv9~J`>K({Hr~2Y*9Ij5H~lCx~D>`vb6I|a6Uj_!0_WgY5$5VxJKTO zSk6e*gdg diff --git a/site/assets/Chart.js b/site/assets/Chart.js deleted file mode 100755 index a25acef1d6b..00000000000 --- a/site/assets/Chart.js +++ /dev/null @@ -1,1443 +0,0 @@ -/*! - * Chart.js - * http://chartjs.org/ - * - * Copyright 2013 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ - -//Define the global Chart Variable as a class. -var Chart = function(context){ - - var chart = this; - - - //Easing functions adapted from Robert Penner's easing equations - //http://www.robertpenner.com/easing/ - - var animationOptions = { - linear : function (t){ - return t; - }, - easeInQuad: function (t) { - return t*t; - }, - easeOutQuad: function (t) { - return -1 *t*(t-2); - }, - easeInOutQuad: function (t) { - if ((t/=1/2) < 1) return 1/2*t*t; - return -1/2 * ((--t)*(t-2) - 1); - }, - easeInCubic: function (t) { - return t*t*t; - }, - easeOutCubic: function (t) { - return 1*((t=t/1-1)*t*t + 1); - }, - easeInOutCubic: function (t) { - if ((t/=1/2) < 1) return 1/2*t*t*t; - return 1/2*((t-=2)*t*t + 2); - }, - easeInQuart: function (t) { - return t*t*t*t; - }, - easeOutQuart: function (t) { - 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; - return -1/2 * ((t-=2)*t*t*t - 2); - }, - easeInQuint: function (t) { - return 1*(t/=1)*t*t*t*t; - }, - easeOutQuint: function (t) { - 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; - return 1/2*((t-=2)*t*t*t*t + 2); - }, - easeInSine: function (t) { - return -1 * Math.cos(t/1 * (Math.PI/2)) + 1; - }, - easeOutSine: function (t) { - return 1 * Math.sin(t/1 * (Math.PI/2)); - }, - easeInOutSine: function (t) { - return -1/2 * (Math.cos(Math.PI*t/1) - 1); - }, - easeInExpo: function (t) { - return (t==0) ? 1 : 1 * Math.pow(2, 10 * (t/1 - 1)); - }, - easeOutExpo: function (t) { - 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)); - return 1/2 * (-Math.pow(2, -10 * --t) + 2); - }, - easeInCirc: function (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); - 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*.3; - if (a < Math.abs(1)) { a=1; var s=p/4; } - else var 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*.3; - if (a < Math.abs(1)) { a=1; var s=p/4; } - else var 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*(.3*1.5); - if (a < Math.abs(1)) { a=1; var s=p/4; } - else var s = p/(2*Math.PI) * Math.asin (1/a); - if (t < 1) return -.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 )*.5 + 1; - }, - easeInBack: function (t) { - var s = 1.70158; - return 1*(t/=1)*t*((s+1)*t - s); - }, - easeOutBack: function (t) { - var s = 1.70158; - return 1*((t=t/1-1)*t*((s+1)*t + s) + 1); - }, - easeInOutBack: function (t) { - var s = 1.70158; - 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) { - return 1 - animationOptions.easeOutBounce (1-t); - }, - easeOutBounce: function (t) { - if ((t/=1) < (1/2.75)) { - return 1*(7.5625*t*t); - } else if (t < (2/2.75)) { - return 1*(7.5625*(t-=(1.5/2.75))*t + .75); - } else if (t < (2.5/2.75)) { - return 1*(7.5625*(t-=(2.25/2.75))*t + .9375); - } else { - return 1*(7.5625*(t-=(2.625/2.75))*t + .984375); - } - }, - easeInOutBounce: function (t) { - if (t < 1/2) return animationOptions.easeInBounce (t*2) * .5; - return animationOptions.easeOutBounce (t*2-1) * .5 + 1*.5; - } - }; - - //Variables global to the chart - var width = context.canvas.width; - var height = context.canvas.height; - - - //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. - if (window.devicePixelRatio) { - context.canvas.style.width = width + "px"; - context.canvas.style.height = height + "px"; - context.canvas.height = height * window.devicePixelRatio; - context.canvas.width = width * window.devicePixelRatio; - context.scale(window.devicePixelRatio, window.devicePixelRatio); - } - - this.PolarArea = function(data,options){ - - chart.PolarArea.defaults = { - scaleOverlay : true, - scaleOverride : false, - scaleSteps : null, - scaleStepWidth : null, - scaleStartValue : null, - scaleShowLine : true, - scaleLineColor : "rgba(0,0,0,.1)", - scaleLineWidth : 1, - scaleShowLabels : true, - scaleLabel : "<%=value%>", - scaleFontFamily : "'Arial'", - scaleFontSize : 12, - scaleFontStyle : "normal", - scaleFontColor : "#666", - scaleShowLabelBackdrop : true, - scaleBackdropColor : "rgba(255,255,255,0.75)", - scaleBackdropPaddingY : 2, - scaleBackdropPaddingX : 2, - segmentShowStroke : true, - segmentStrokeColor : "#fff", - segmentStrokeWidth : 2, - animation : true, - animationSteps : 100, - animationEasing : "easeOutBounce", - animateRotate : true, - animateScale : false, - onAnimationComplete : null - }; - - var config = (options)? mergeChartConfig(chart.PolarArea.defaults,options) : chart.PolarArea.defaults; - - return new PolarArea(data,config,context); - }; - - this.Radar = function(data,options){ - - chart.Radar.defaults = { - scaleOverlay : false, - scaleOverride : false, - scaleSteps : null, - scaleStepWidth : null, - scaleStartValue : null, - scaleShowLine : true, - scaleLineColor : "rgba(0,0,0,.1)", - scaleLineWidth : 1, - scaleShowLabels : false, - scaleLabel : "<%=value%>", - scaleFontFamily : "'Arial'", - scaleFontSize : 12, - scaleFontStyle : "normal", - scaleFontColor : "#666", - scaleShowLabelBackdrop : true, - scaleBackdropColor : "rgba(255,255,255,0.75)", - scaleBackdropPaddingY : 2, - scaleBackdropPaddingX : 2, - angleShowLineOut : true, - angleLineColor : "rgba(0,0,0,.1)", - angleLineWidth : 1, - pointLabelFontFamily : "'Arial'", - pointLabelFontStyle : "normal", - pointLabelFontSize : 12, - pointLabelFontColor : "#666", - pointDot : true, - pointDotRadius : 3, - pointDotStrokeWidth : 1, - datasetStroke : true, - datasetStrokeWidth : 2, - datasetFill : true, - animation : true, - animationSteps : 60, - animationEasing : "easeOutQuart", - onAnimationComplete : null - }; - - var config = (options)? mergeChartConfig(chart.Radar.defaults,options) : chart.Radar.defaults; - - return new Radar(data,config,context); - }; - - this.Pie = function(data,options){ - chart.Pie.defaults = { - segmentShowStroke : true, - segmentStrokeColor : "#fff", - segmentStrokeWidth : 2, - animation : true, - animationSteps : 100, - animationEasing : "easeOutBounce", - animateRotate : true, - animateScale : false, - onAnimationComplete : null - }; - - var config = (options)? mergeChartConfig(chart.Pie.defaults,options) : chart.Pie.defaults; - - return new Pie(data,config,context); - }; - - this.Doughnut = function(data,options){ - - chart.Doughnut.defaults = { - segmentShowStroke : true, - segmentStrokeColor : "#fff", - segmentStrokeWidth : 2, - percentageInnerCutout : 50, - animation : true, - animationSteps : 100, - animationEasing : "easeOutBounce", - animateRotate : true, - animateScale : false, - onAnimationComplete : null - }; - - var config = (options)? mergeChartConfig(chart.Doughnut.defaults,options) : chart.Doughnut.defaults; - - return new Doughnut(data,config,context); - - }; - - this.Line = function(data,options){ - - chart.Line.defaults = { - scaleOverlay : false, - scaleOverride : false, - scaleSteps : null, - scaleStepWidth : null, - scaleStartValue : null, - scaleLineColor : "rgba(0,0,0,.1)", - scaleLineWidth : 1, - scaleShowLabels : true, - scaleLabel : "<%=value%>", - scaleFontFamily : "'Arial'", - scaleFontSize : 12, - scaleFontStyle : "normal", - scaleFontColor : "#666", - scaleShowGridLines : true, - scaleGridLineColor : "rgba(0,0,0,.05)", - scaleGridLineWidth : 1, - bezierCurve : true, - pointDot : true, - pointDotRadius : 4, - pointDotStrokeWidth : 2, - datasetStroke : true, - datasetStrokeWidth : 2, - datasetFill : true, - animation : true, - animationSteps : 60, - animationEasing : "easeOutQuart", - onAnimationComplete : null - }; - var config = (options) ? mergeChartConfig(chart.Line.defaults,options) : chart.Line.defaults; - - return new Line(data,config,context); - } - - this.Bar = function(data,options){ - chart.Bar.defaults = { - scaleOverlay : false, - scaleOverride : false, - scaleSteps : null, - scaleStepWidth : null, - scaleStartValue : null, - scaleLineColor : "rgba(0,0,0,.1)", - scaleLineWidth : 1, - scaleShowLabels : true, - scaleLabel : "<%=value%>", - scaleFontFamily : "'Arial'", - scaleFontSize : 12, - scaleFontStyle : "normal", - scaleFontColor : "#666", - scaleShowGridLines : true, - scaleGridLineColor : "rgba(0,0,0,.05)", - scaleGridLineWidth : 1, - barShowStroke : true, - barStrokeWidth : 2, - barValueSpacing : 5, - barDatasetSpacing : 1, - animation : true, - animationSteps : 60, - animationEasing : "easeOutQuart", - onAnimationComplete : null - }; - var config = (options) ? mergeChartConfig(chart.Bar.defaults,options) : chart.Bar.defaults; - - return new Bar(data,config,context); - } - - var clear = function(c){ - c.clearRect(0, 0, width, height); - }; - - var PolarArea = function(data,config,ctx){ - var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString; - - - calculateDrawingSizes(); - - valueBounds = getValueBounds(); - - labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : null; - - //Check and set the scale - if (!config.scaleOverride){ - - calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); - } - else { - calculatedScale = { - steps : config.scaleSteps, - stepValue : config.scaleStepWidth, - graphMin : config.scaleStartValue, - labels : [] - } - for (var i=0; i upperValue) {upperValue = data[i].value;} - if (data[i].value < lowerValue) {lowerValue = data[i].value;} - }; - - var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); - var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); - - return { - maxValue : upperValue, - minValue : lowerValue, - maxSteps : maxSteps, - minSteps : minSteps - }; - - - } - } - - var Radar = function (data,config,ctx) { - var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString; - - //If no labels are defined set to an empty array, so referencing length for looping doesn't blow up. - if (!data.labels) data.labels = []; - - calculateDrawingSizes(); - - var valueBounds = getValueBounds(); - - labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : null; - - //Check and set the scale - if (!config.scaleOverride){ - - calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); - } - else { - calculatedScale = { - steps : config.scaleSteps, - stepValue : config.scaleStepWidth, - graphMin : config.scaleStartValue, - labels : [] - } - for (var i=0; i Math.PI){ - ctx.textAlign = "right"; - } - else{ - ctx.textAlign = "left"; - } - - ctx.textBaseline = "middle"; - - ctx.fillText(data.labels[k],opposite,-adjacent); - - } - ctx.restore(); - }; - function calculateDrawingSizes(){ - maxSize = (Min([width,height])/2); - - labelHeight = config.scaleFontSize*2; - - var labelLength = 0; - for (var i=0; ilabelLength) labelLength = textMeasurement; - } - - //Figure out whats the largest - the height of the text or the width of what's there, and minus it from the maximum usable size. - maxSize -= Max([labelLength,((config.pointLabelFontSize/2)*1.5)]); - - maxSize -= config.pointLabelFontSize; - maxSize = CapValue(maxSize, null, 0); - scaleHeight = maxSize; - //If the label height is less than 5, set it to 5 so we don't have lines on top of each other. - labelHeight = Default(labelHeight,5); - }; - function getValueBounds() { - var upperValue = Number.MIN_VALUE; - var lowerValue = Number.MAX_VALUE; - - for (var i=0; i upperValue){upperValue = data.datasets[i].data[j]} - if (data.datasets[i].data[j] < lowerValue){lowerValue = data.datasets[i].data[j]} - } - } - - var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); - var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); - - return { - maxValue : upperValue, - minValue : lowerValue, - maxSteps : maxSteps, - minSteps : minSteps - }; - - - } - } - - var Pie = function(data,config,ctx){ - var segmentTotal = 0; - - //In case we have a canvas that is not a square. Minus 5 pixels as padding round the edge. - var pieRadius = Min([height/2,width/2]) - 5; - - for (var i=0; i 0){ - ctx.save(); - ctx.textAlign = "right"; - } - else{ - ctx.textAlign = "center"; - } - ctx.fillStyle = config.scaleFontColor; - for (var i=0; i 0){ - ctx.translate(yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize); - ctx.rotate(-(rotateLabels * (Math.PI/180))); - ctx.fillText(data.labels[i], 0,0); - ctx.restore(); - } - - else{ - ctx.fillText(data.labels[i], yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize+3); - } - - ctx.beginPath(); - ctx.moveTo(yAxisPosX + i * valueHop, xAxisPosY+3); - - //Check i isnt 0, so we dont go over the Y axis twice. - if(config.scaleShowGridLines && i>0){ - ctx.lineWidth = config.scaleGridLineWidth; - ctx.strokeStyle = config.scaleGridLineColor; - ctx.lineTo(yAxisPosX + i * valueHop, 5); - } - else{ - ctx.lineTo(yAxisPosX + i * valueHop, xAxisPosY+3); - } - ctx.stroke(); - } - - //Y axis - ctx.lineWidth = config.scaleLineWidth; - ctx.strokeStyle = config.scaleLineColor; - ctx.beginPath(); - ctx.moveTo(yAxisPosX,xAxisPosY+5); - ctx.lineTo(yAxisPosX,5); - ctx.stroke(); - - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - for (var j=0; j longestText)? measuredText : longestText; - } - //Add a little extra padding from the y axis - longestText +=10; - } - xAxisLength = width - longestText - widestXLabel; - valueHop = Math.floor(xAxisLength/(data.labels.length-1)); - - yAxisPosX = width-widestXLabel/2-xAxisLength; - xAxisPosY = scaleHeight + config.scaleFontSize/2; - } - function calculateDrawingSizes(){ - maxSize = height; - - //Need to check the X axis first - measure the length of each text metric, and figure out if we need to rotate by 45 degrees. - ctx.font = config.scaleFontStyle + " " + config.scaleFontSize+"px " + config.scaleFontFamily; - widestXLabel = 1; - for (var i=0; i widestXLabel)? textLength : widestXLabel; - } - if (width/data.labels.length < widestXLabel){ - rotateLabels = 45; - if (width/data.labels.length < Math.cos(rotateLabels) * widestXLabel){ - rotateLabels = 90; - maxSize -= widestXLabel; - } - else{ - maxSize -= Math.sin(rotateLabels) * widestXLabel; - } - } - else{ - maxSize -= config.scaleFontSize; - } - - //Add a little padding between the x line and the text - maxSize -= 5; - - - labelHeight = config.scaleFontSize; - - maxSize -= labelHeight; - //Set 5 pixels greater than the font size to allow for a little padding from the X axis. - - scaleHeight = maxSize; - - //Then get the area above we can safely draw on. - - } - function getValueBounds() { - var upperValue = Number.MIN_VALUE; - var lowerValue = Number.MAX_VALUE; - for (var i=0; i upperValue) { upperValue = data.datasets[i].data[j] }; - if ( data.datasets[i].data[j] < lowerValue) { lowerValue = data.datasets[i].data[j] }; - } - }; - - var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); - var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); - - return { - maxValue : upperValue, - minValue : lowerValue, - maxSteps : maxSteps, - minSteps : minSteps - }; - - - } - - - } - - var Bar = function(data,config,ctx){ - var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString, valueHop,widestXLabel, xAxisLength,yAxisPosX,xAxisPosY,barWidth, rotateLabels = 0; - - calculateDrawingSizes(); - - valueBounds = getValueBounds(); - //Check and set the scale - labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : ""; - if (!config.scaleOverride){ - - calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); - } - else { - calculatedScale = { - steps : config.scaleSteps, - stepValue : config.scaleStepWidth, - graphMin : config.scaleStartValue, - labels : [] - } - for (var i=0; i 0){ - ctx.save(); - ctx.textAlign = "right"; - } - else{ - ctx.textAlign = "center"; - } - ctx.fillStyle = config.scaleFontColor; - for (var i=0; i 0){ - ctx.translate(yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize); - ctx.rotate(-(rotateLabels * (Math.PI/180))); - ctx.fillText(data.labels[i], 0,0); - ctx.restore(); - } - - else{ - ctx.fillText(data.labels[i], yAxisPosX + i*valueHop + valueHop/2,xAxisPosY + config.scaleFontSize+3); - } - - ctx.beginPath(); - ctx.moveTo(yAxisPosX + (i+1) * valueHop, xAxisPosY+3); - - //Check i isnt 0, so we dont go over the Y axis twice. - ctx.lineWidth = config.scaleGridLineWidth; - ctx.strokeStyle = config.scaleGridLineColor; - ctx.lineTo(yAxisPosX + (i+1) * valueHop, 5); - ctx.stroke(); - } - - //Y axis - ctx.lineWidth = config.scaleLineWidth; - ctx.strokeStyle = config.scaleLineColor; - ctx.beginPath(); - ctx.moveTo(yAxisPosX,xAxisPosY+5); - ctx.lineTo(yAxisPosX,5); - ctx.stroke(); - - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - for (var j=0; j longestText)? measuredText : longestText; - } - //Add a little extra padding from the y axis - longestText +=10; - } - xAxisLength = width - longestText - widestXLabel; - valueHop = Math.floor(xAxisLength/(data.labels.length)); - - barWidth = (valueHop - config.scaleGridLineWidth*2 - (config.barValueSpacing*2) - (config.barDatasetSpacing*data.datasets.length-1) - ((config.barStrokeWidth/2)*data.datasets.length-1))/data.datasets.length; - - yAxisPosX = width-widestXLabel/2-xAxisLength; - xAxisPosY = scaleHeight + config.scaleFontSize/2; - } - function calculateDrawingSizes(){ - maxSize = height; - - //Need to check the X axis first - measure the length of each text metric, and figure out if we need to rotate by 45 degrees. - ctx.font = config.scaleFontStyle + " " + config.scaleFontSize+"px " + config.scaleFontFamily; - widestXLabel = 1; - for (var i=0; i widestXLabel)? textLength : widestXLabel; - } - if (width/data.labels.length < widestXLabel){ - rotateLabels = 45; - if (width/data.labels.length < Math.cos(rotateLabels) * widestXLabel){ - rotateLabels = 90; - maxSize -= widestXLabel; - } - else{ - maxSize -= Math.sin(rotateLabels) * widestXLabel; - } - } - else{ - maxSize -= config.scaleFontSize; - } - - //Add a little padding between the x line and the text - maxSize -= 5; - - - labelHeight = config.scaleFontSize; - - maxSize -= labelHeight; - //Set 5 pixels greater than the font size to allow for a little padding from the X axis. - - scaleHeight = maxSize; - - //Then get the area above we can safely draw on. - - } - function getValueBounds() { - var upperValue = Number.MIN_VALUE; - var lowerValue = Number.MAX_VALUE; - for (var i=0; i upperValue) { upperValue = data.datasets[i].data[j] }; - if ( data.datasets[i].data[j] < lowerValue) { lowerValue = data.datasets[i].data[j] }; - } - }; - - var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); - var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); - - return { - maxValue : upperValue, - minValue : lowerValue, - maxSteps : maxSteps, - minSteps : minSteps - }; - - - } - } - - function calculateOffset(val,calculatedScale,scaleHop){ - var outerValue = calculatedScale.steps * calculatedScale.stepValue; - var adjustedValue = val - calculatedScale.graphMin; - var scalingFactor = CapValue(adjustedValue/outerValue,1,0); - return (scaleHop*calculatedScale.steps) * scalingFactor; - } - - function animationLoop(config,drawScale,drawData,ctx){ - var animFrameAmount = (config.animation)? 1/CapValue(config.animationSteps,Number.MAX_VALUE,1) : 1, - easingFunction = animationOptions[config.animationEasing], - percentAnimComplete =(config.animation)? 0 : 1; - - - - if (typeof drawScale !== "function") drawScale = function(){}; - - requestAnimFrame(animLoop); - - function animateFrame(){ - var easeAdjustedAnimationPercent =(config.animation)? CapValue(easingFunction(percentAnimComplete),null,0) : 1; - clear(ctx); - if(config.scaleOverlay){ - drawData(easeAdjustedAnimationPercent); - drawScale(); - } else { - drawScale(); - drawData(easeAdjustedAnimationPercent); - } - } - function animLoop(){ - //We need to check if the animation is incomplete (less than 1), or complete (1). - percentAnimComplete += animFrameAmount; - animateFrame(); - //Stop the loop continuing forever - if (percentAnimComplete <= 1){ - requestAnimFrame(animLoop); - } - else{ - if (typeof config.onAnimationComplete == "function") config.onAnimationComplete(); - } - - } - - } - - //Declare global functions to be called within this namespace here. - - - // shim layer with setTimeout fallback - var requestAnimFrame = (function(){ - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback) { - window.setTimeout(callback, 1000 / 60); - }; - })(); - - function calculateScale(drawingHeight,maxSteps,minSteps,maxValue,minValue,labelTemplateString){ - var graphMin,graphMax,graphRange,stepValue,numberOfSteps,valueRange,rangeOrderOfMagnitude,decimalNum; - - valueRange = maxValue - minValue; - - rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange); - - graphMin = Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude); - - graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude); - - graphRange = graphMax - graphMin; - - stepValue = Math.pow(10, rangeOrderOfMagnitude); - - numberOfSteps = Math.round(graphRange / stepValue); - - //Compare number of steps to the max and min for that size graph, and add in half steps if need be. - while(numberOfSteps < minSteps || numberOfSteps > maxSteps) { - if (numberOfSteps < minSteps){ - stepValue /= 2; - numberOfSteps = Math.round(graphRange/stepValue); - } - else{ - stepValue *=2; - numberOfSteps = Math.round(graphRange/stepValue); - } - }; - - - - //Create an array of all the labels by interpolating the string. - - var labels = []; - - if(labelTemplateString){ - //Fix floating point errors by setting to fixed the on the same decimal as the stepValue. - for (var i=1; i maxValue ) { - return maxValue; - } - } - if(isNumber(minValue)){ - if ( valueToCap < minValue ){ - return minValue; - } - } - return valueToCap; - } - function getDecimalPlaces (num){ - var numberOfDecimalPlaces; - if (num%1!=0){ - return num.toString().split(".")[1].length - } - else{ - return 0; - } - - } - - function mergeChartConfig(defaults,userDefined){ - var returnObj = {}; - for (var attrname in defaults) { returnObj[attrname] = defaults[attrname]; } - for (var attrname in userDefined) { returnObj[attrname] = userDefined[attrname]; } - return returnObj; - } - - //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ - var cache = {}; - - function tmpl(str, data){ - // Figure out if we're getting a template, or if we need to - // load the template - and be sure to cache the result. - var fn = !/\W/.test(str) ? - cache[str] = cache[str] || - tmpl(document.getElementById(str).innerHTML) : - - // Generate a reusable function that will serve as a template - // generator (and which will be cached). - new Function("obj", - "var p=[],print=function(){p.push.apply(p,arguments);};" + - - // Introduce the data as local variables using with(){} - "with(obj){p.push('" + - - // Convert the template into pure JavaScript - str - .replace(/[\r\t\n]/g, " ") - .split("<%").join("\t") - .replace(/((^|%>)[^\t]*)'/g, "$1\r") - .replace(/\t=(.*?)%>/g, "',$1,'") - .split("\t").join("');") - .split("%>").join("p.push('") - .split("\r").join("\\'") - + "');}return p.join('');"); - - // Provide some basic currying to the user - return data ? fn( data ) : fn; - }; -} - - - diff --git a/site/assets/effects.js b/site/assets/effects.js deleted file mode 100644 index 19361f23fc8..00000000000 --- a/site/assets/effects.js +++ /dev/null @@ -1,320 +0,0 @@ -$(window).load(function() { - var lineChartData = { - labels : ["January","February","March","April","May","June","July"], - datasets : [ - { - fillColor : "rgba(220,220,220,0.5)", - strokeColor : "rgba(220,220,220,1)", - pointColor : "rgba(220,220,220,1)", - pointStrokeColor : "#fff", - data : [65,59,90,81,56,55,40] - }, - { - fillColor : "rgba(151,187,205,0.5)", - strokeColor : "rgba(151,187,205,1)", - pointColor : "rgba(151,187,205,1)", - pointStrokeColor : "#fff", - data : [28,48,40,19,96,27,100] - } - ] - }; - - var barChartData = { - labels : ["January","February","March","April","May","June","July"], - datasets : [ - { - fillColor : "rgba(220,220,220,0.5)", - strokeColor : "rgba(220,220,220,1)", - data : [65,59,90,81,56,55,40] - }, - { - fillColor : "rgba(151,187,205,0.5)", - strokeColor : "rgba(151,187,205,1)", - data : [28,48,40,19,96,27,100] - } - ] - - }; - - var radarChartData = { - labels : ["A","B","C","D","E","F","G"], - datasets : [ - { - fillColor : "rgba(220,220,220,0.5)", - strokeColor : "rgba(220,220,220,1)", - pointColor : "rgba(220,220,220,1)", - pointStrokeColor : "#fff", - data : [65,59,90,81,56,55,40] - }, - { - fillColor : "rgba(151,187,205,0.5)", - strokeColor : "rgba(151,187,205,1)", - pointColor : "rgba(151,187,205,1)", - pointStrokeColor : "#fff", - data : [28,48,40,19,96,27,100] - } - ] - - }; - var pieChartData = [ - { - value: 30, - color:"#F38630" - }, - { - value : 50, - color : "#E0E4CC" - }, - { - value : 100, - color : "#69D2E7" - } - - ]; - var polarAreaChartData = [ - { - value : 62, - color: "#D97041" - }, - { - value : 70, - color: "#C7604C" - }, - { - value : 41, - color: "#21323D" - }, - { - value : 24, - color: "#9D9B7F" - }, - { - value : 55, - color: "#7D4F6D" - }, - { - value : 18, - color: "#584A5E" - } - ]; - var doughnutChartData = [ - { - value: 30, - color:"#F7464A" - }, - { - value : 50, - color : "#46BFBD" - }, - { - value : 100, - color : "#FDB45C" - }, - { - value : 40, - color : "#949FB1" - }, - { - value : 120, - color : "#4D5360" - } - - ]; - - var globalGraphSettings = {animation : Modernizr.canvas}; - - setIntroChart(); - - function setIntroChart(){ - var ctx = document.getElementById("introChart").getContext("2d"); - - new Chart(ctx).Line(lineChartData,{animation: Modernizr.canvas, scaleShowLabels : false, scaleFontColor : "#767C8D"}); - }; - - function showLineChart(){ - var ctx = document.getElementById("lineChartCanvas").getContext("2d"); - new Chart(ctx).Line(lineChartData,globalGraphSettings); - }; - function showBarChart(){ - var ctx = document.getElementById("barChartCanvas").getContext("2d"); - new Chart(ctx).Bar(barChartData,globalGraphSettings); - }; - function showRadarChart(){ - var ctx = document.getElementById("radarChartCanvas").getContext("2d"); - new Chart(ctx).Radar(radarChartData,globalGraphSettings); - } - function showPolarAreaChart(){ - var ctx = document.getElementById("polarAreaChartCanvas").getContext("2d"); - new Chart(ctx).PolarArea(polarAreaChartData,globalGraphSettings); - } - function showPieChart(){ - var ctx = document.getElementById("pieChartCanvas").getContext("2d"); - new Chart(ctx).Pie(pieChartData,globalGraphSettings); - }; - function showDoughnutChart(){ - var ctx = document.getElementById("doughnutChartCanvas").getContext("2d"); - new Chart(ctx).Doughnut(doughnutChartData,globalGraphSettings); - }; - - var graphInitDelay = 300; - - //Set up each of the inview events here. - $("#lineChart").on("inview",function(){ - var $this = $(this); - $this.removeClass("hidden").off("inview"); - setTimeout(showLineChart,graphInitDelay); - }); - $("#barChart").on("inview",function(){ - var $this = $(this); - $this.removeClass("hidden").off("inview"); - setTimeout(showBarChart,graphInitDelay); - }); - - $("#radarChart").on("inview",function(){ - var $this = $(this); - $this.removeClass("hidden").off("inview"); - setTimeout(showRadarChart,graphInitDelay); - }); - $("#pieChart").on("inview",function(){ - var $this = $(this); - $this.removeClass("hidden").off("inview"); - setTimeout(showPieChart,graphInitDelay); - }); - $("#polarAreaChart").on("inview",function(){ - var $this = $(this); - $this.removeClass("hidden").off("inview"); - setTimeout(showPolarAreaChart,graphInitDelay); - }); - $("#doughnutChart").on("inview",function(){ - var $this = $(this); - $this.removeClass("hidden").off("inview"); - setTimeout(showDoughnutChart,graphInitDelay); - }); - - }); - - /** - * author Christopher Blum - * - based on the idea of Remy Sharp, http://remysharp.com/2009/01/26/element-in-view-event-plugin/ - * - forked from http://github.com/zuk/jquery.inview/ - */ - (function ($) { - var inviewObjects = {}, viewportSize, viewportOffset, - d = document, w = window, documentElement = d.documentElement, expando = $.expando; - - $.event.special.inview = { - add: function(data) { - inviewObjects[data.guid + "-" + this[expando]] = { data: data, $element: $(this) }; - }, - - remove: function(data) { - try { delete inviewObjects[data.guid + "-" + this[expando]]; } catch(e) {} - } - }; - - function getViewportSize() { - var mode, domObject, size = { height: w.innerHeight, width: w.innerWidth }; - - // if this is correct then return it. iPad has compat Mode, so will - // go into check clientHeight/clientWidth (which has the wrong value). - if (!size.height) { - mode = d.compatMode; - if (mode || !$.support.boxModel) { // IE, Gecko - domObject = mode === 'CSS1Compat' ? - documentElement : // Standards - d.body; // Quirks - size = { - height: domObject.clientHeight, - width: domObject.clientWidth - }; - } - } - - return size; - } - - function getViewportOffset() { - return { - top: w.pageYOffset || documentElement.scrollTop || d.body.scrollTop, - left: w.pageXOffset || documentElement.scrollLeft || d.body.scrollLeft - }; - } - - function checkInView() { - var $elements = $(), elementsLength, i = 0; - - $.each(inviewObjects, function(i, inviewObject) { - var selector = inviewObject.data.selector, - $element = inviewObject.$element; - $elements = $elements.add(selector ? $element.find(selector) : $element); - }); - - elementsLength = $elements.length; - if (elementsLength) { - viewportSize = viewportSize || getViewportSize(); - viewportOffset = viewportOffset || getViewportOffset(); - - for (; i viewportOffset.top && - elementOffset.top < viewportOffset.top + viewportSize.height && - elementOffset.left + elementSize.width > viewportOffset.left && - elementOffset.left < viewportOffset.left + viewportSize.width) { - visiblePartX = (viewportOffset.left > elementOffset.left ? - 'right' : (viewportOffset.left + viewportSize.width) < (elementOffset.left + elementSize.width) ? - 'left' : 'both'); - visiblePartY = (viewportOffset.top > elementOffset.top ? - 'bottom' : (viewportOffset.top + viewportSize.height) < (elementOffset.top + elementSize.height) ? - 'top' : 'both'); - visiblePartsMerged = visiblePartX + "-" + visiblePartY; - if (!inView || inView !== visiblePartsMerged) { - $element.data('inview', visiblePartsMerged).trigger('inview', [true, visiblePartX, visiblePartY]); - } - } else if (inView) { - $element.data('inview', false).trigger('inview', [false]); - } - } - } - } - - $(w).bind("scroll resize", function() { - viewportSize = viewportOffset = null; - }); - - // IE < 9 scrolls to focused elements without firing the "scroll" event - if (!documentElement.addEventListener && documentElement.attachEvent) { - documentElement.attachEvent("onfocusin", function() { - viewportOffset = null; - }); - } - - // Use setInterval in order to also make sure this captures elements within - // "overflow:scroll" elements or elements that appeared in the dom tree due to - // dom manipulation and reflow - // old: $(window).scroll(checkInView); - // - // By the way, iOS (iPad, iPhone, ...) seems to not execute, or at least delays - // intervals while the user scrolls. Therefore the inview event might fire a bit late there - setInterval(checkInView, 250); - })(jQuery); \ No newline at end of file diff --git a/site/assets/excanvas.js b/site/assets/excanvas.js deleted file mode 100644 index 00b316ca9e1..00000000000 --- a/site/assets/excanvas.js +++ /dev/null @@ -1,1416 +0,0 @@ -// Copyright 2006 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -// Known Issues: -// -// * Patterns only support repeat. -// * Radial gradient are not implemented. The VML version of these look very -// different from the canvas one. -// * Clipping paths are not implemented. -// * Coordsize. The width and height attribute have higher priority than the -// width and height style values which isn't correct. -// * Painting mode isn't implemented. -// * Canvas width/height should is using content-box by default. IE in -// Quirks mode will draw the canvas using border-box. Either change your -// doctype to HTML5 -// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype) -// or use Box Sizing Behavior from WebFX -// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html) -// * Non uniform scaling does not correctly scale strokes. -// * Optimize. There is always room for speed improvements. - -// Only add this code if we do not already have a canvas implementation -if (!document.createElement('canvas').getContext) { - -(function() { - - // alias some functions to make (compiled) code shorter - var m = Math; - var mr = m.round; - var ms = m.sin; - var mc = m.cos; - var abs = m.abs; - var sqrt = m.sqrt; - - // this is used for sub pixel precision - var Z = 10; - var Z2 = Z / 2; - - var IE_VERSION = +navigator.userAgent.match(/MSIE ([\d.]+)?/)[1]; - - /** - * This funtion is assigned to the elements as element.getContext(). - * @this {HTMLElement} - * @return {CanvasRenderingContext2D_} - */ - function getContext() { - return this.context_ || - (this.context_ = new CanvasRenderingContext2D_(this)); - } - - var slice = Array.prototype.slice; - - /** - * Binds a function to an object. The returned function will always use the - * passed in {@code obj} as {@code this}. - * - * Example: - * - * g = bind(f, obj, a, b) - * g(c, d) // will do f.call(obj, a, b, c, d) - * - * @param {Function} f The function to bind the object to - * @param {Object} obj The object that should act as this when the function - * is called - * @param {*} var_args Rest arguments that will be used as the initial - * arguments when the function is called - * @return {Function} A new function that has bound this - */ - function bind(f, obj, var_args) { - var a = slice.call(arguments, 2); - return function() { - return f.apply(obj, a.concat(slice.call(arguments))); - }; - } - - function encodeHtmlAttribute(s) { - return String(s).replace(/&/g, '&').replace(/"/g, '"'); - } - - function addNamespace(doc, prefix, urn) { - if (!doc.namespaces[prefix]) { - doc.namespaces.add(prefix, urn, '#default#VML'); - } - } - - function addNamespacesAndStylesheet(doc) { - addNamespace(doc, 'g_vml_', 'urn:schemas-microsoft-com:vml'); - addNamespace(doc, 'g_o_', 'urn:schemas-microsoft-com:office:office'); - - // Setup default CSS. Only add one style sheet per document - if (!doc.styleSheets['ex_canvas_']) { - var ss = doc.createStyleSheet(); - ss.owningElement.id = 'ex_canvas_'; - ss.cssText = 'canvas{display:inline-block;overflow:hidden;' + - // default size is 300x150 in Gecko and Opera - 'text-align:left;width:300px;height:150px}'; - } - } - - // Add namespaces and stylesheet at startup. - addNamespacesAndStylesheet(document); - - var G_vmlCanvasManager_ = { - init: function(opt_doc) { - var doc = opt_doc || document; - // Create a dummy element so that IE will allow canvas elements to be - // recognized. - doc.createElement('canvas'); - doc.attachEvent('onreadystatechange', bind(this.init_, this, doc)); - }, - - init_: function(doc) { - // find all canvas elements - var els = doc.getElementsByTagName('canvas'); - for (var i = 0; i < els.length; i++) { - this.initElement(els[i]); - } - }, - - /** - * Public initializes a canvas element so that it can be used as canvas - * element from now on. This is called automatically before the page is - * loaded but if you are creating elements using createElement you need to - * make sure this is called on the element. - * @param {HTMLElement} el The canvas element to initialize. - * @return {HTMLElement} the element that was created. - */ - initElement: function(el) { - if (!el.getContext) { - el.getContext = getContext; - - // Add namespaces and stylesheet to document of the element. - addNamespacesAndStylesheet(el.ownerDocument); - - // Remove fallback content. There is no way to hide text nodes so we - // just remove all childNodes. We could hide all elements and remove - // text nodes but who really cares about the fallback content. - el.innerHTML = ''; - - // do not use inline function because that will leak memory - el.attachEvent('onpropertychange', onPropertyChange); - el.attachEvent('onresize', onResize); - - var attrs = el.attributes; - if (attrs.width && attrs.width.specified) { - // TODO: use runtimeStyle and coordsize - // el.getContext().setWidth_(attrs.width.nodeValue); - el.style.width = attrs.width.nodeValue + 'px'; - } else { - el.width = el.clientWidth; - } - if (attrs.height && attrs.height.specified) { - // TODO: use runtimeStyle and coordsize - // el.getContext().setHeight_(attrs.height.nodeValue); - el.style.height = attrs.height.nodeValue + 'px'; - } else { - el.height = el.clientHeight; - } - //el.getContext().setCoordsize_() - } - return el; - } - }; - - function onPropertyChange(e) { - var el = e.srcElement; - - switch (e.propertyName) { - case 'width': - el.getContext().clearRect(); - el.style.width = el.attributes.width.nodeValue + 'px'; - // In IE8 this does not trigger onresize. - el.firstChild.style.width = el.clientWidth + 'px'; - break; - case 'height': - el.getContext().clearRect(); - el.style.height = el.attributes.height.nodeValue + 'px'; - el.firstChild.style.height = el.clientHeight + 'px'; - break; - } - } - - function onResize(e) { - var el = e.srcElement; - if (el.firstChild) { - el.firstChild.style.width = el.clientWidth + 'px'; - el.firstChild.style.height = el.clientHeight + 'px'; - } - } - - G_vmlCanvasManager_.init(); - - // precompute "00" to "FF" - var decToHex = []; - for (var i = 0; i < 16; i++) { - for (var j = 0; j < 16; j++) { - decToHex[i * 16 + j] = i.toString(16) + j.toString(16); - } - } - - function createMatrixIdentity() { - return [ - [1, 0, 0], - [0, 1, 0], - [0, 0, 1] - ]; - } - - function matrixMultiply(m1, m2) { - var result = createMatrixIdentity(); - - for (var x = 0; x < 3; x++) { - for (var y = 0; y < 3; y++) { - var sum = 0; - - for (var z = 0; z < 3; z++) { - sum += m1[x][z] * m2[z][y]; - } - - result[x][y] = sum; - } - } - return result; - } - - function copyState(o1, o2) { - o2.fillStyle = o1.fillStyle; - o2.lineCap = o1.lineCap; - o2.lineJoin = o1.lineJoin; - o2.lineWidth = o1.lineWidth; - o2.miterLimit = o1.miterLimit; - o2.shadowBlur = o1.shadowBlur; - o2.shadowColor = o1.shadowColor; - o2.shadowOffsetX = o1.shadowOffsetX; - o2.shadowOffsetY = o1.shadowOffsetY; - o2.strokeStyle = o1.strokeStyle; - o2.globalAlpha = o1.globalAlpha; - o2.font = o1.font; - o2.textAlign = o1.textAlign; - o2.textBaseline = o1.textBaseline; - o2.arcScaleX_ = o1.arcScaleX_; - o2.arcScaleY_ = o1.arcScaleY_; - o2.lineScale_ = o1.lineScale_; - } - - var colorData = { - aliceblue: '#F0F8FF', - antiquewhite: '#FAEBD7', - aquamarine: '#7FFFD4', - azure: '#F0FFFF', - beige: '#F5F5DC', - bisque: '#FFE4C4', - black: '#000000', - blanchedalmond: '#FFEBCD', - blueviolet: '#8A2BE2', - brown: '#A52A2A', - burlywood: '#DEB887', - cadetblue: '#5F9EA0', - chartreuse: '#7FFF00', - chocolate: '#D2691E', - coral: '#FF7F50', - cornflowerblue: '#6495ED', - cornsilk: '#FFF8DC', - crimson: '#DC143C', - cyan: '#00FFFF', - darkblue: '#00008B', - darkcyan: '#008B8B', - darkgoldenrod: '#B8860B', - darkgray: '#A9A9A9', - darkgreen: '#006400', - darkgrey: '#A9A9A9', - darkkhaki: '#BDB76B', - darkmagenta: '#8B008B', - darkolivegreen: '#556B2F', - darkorange: '#FF8C00', - darkorchid: '#9932CC', - darkred: '#8B0000', - darksalmon: '#E9967A', - darkseagreen: '#8FBC8F', - darkslateblue: '#483D8B', - darkslategray: '#2F4F4F', - darkslategrey: '#2F4F4F', - darkturquoise: '#00CED1', - darkviolet: '#9400D3', - deeppink: '#FF1493', - deepskyblue: '#00BFFF', - dimgray: '#696969', - dimgrey: '#696969', - dodgerblue: '#1E90FF', - firebrick: '#B22222', - floralwhite: '#FFFAF0', - forestgreen: '#228B22', - gainsboro: '#DCDCDC', - ghostwhite: '#F8F8FF', - gold: '#FFD700', - goldenrod: '#DAA520', - grey: '#808080', - greenyellow: '#ADFF2F', - honeydew: '#F0FFF0', - hotpink: '#FF69B4', - indianred: '#CD5C5C', - indigo: '#4B0082', - ivory: '#FFFFF0', - khaki: '#F0E68C', - lavender: '#E6E6FA', - lavenderblush: '#FFF0F5', - lawngreen: '#7CFC00', - lemonchiffon: '#FFFACD', - lightblue: '#ADD8E6', - lightcoral: '#F08080', - lightcyan: '#E0FFFF', - lightgoldenrodyellow: '#FAFAD2', - lightgreen: '#90EE90', - lightgrey: '#D3D3D3', - lightpink: '#FFB6C1', - lightsalmon: '#FFA07A', - lightseagreen: '#20B2AA', - lightskyblue: '#87CEFA', - lightslategray: '#778899', - lightslategrey: '#778899', - lightsteelblue: '#B0C4DE', - lightyellow: '#FFFFE0', - limegreen: '#32CD32', - linen: '#FAF0E6', - magenta: '#FF00FF', - mediumaquamarine: '#66CDAA', - mediumblue: '#0000CD', - mediumorchid: '#BA55D3', - mediumpurple: '#9370DB', - mediumseagreen: '#3CB371', - mediumslateblue: '#7B68EE', - mediumspringgreen: '#00FA9A', - mediumturquoise: '#48D1CC', - mediumvioletred: '#C71585', - midnightblue: '#191970', - mintcream: '#F5FFFA', - mistyrose: '#FFE4E1', - moccasin: '#FFE4B5', - navajowhite: '#FFDEAD', - oldlace: '#FDF5E6', - olivedrab: '#6B8E23', - orange: '#FFA500', - orangered: '#FF4500', - orchid: '#DA70D6', - palegoldenrod: '#EEE8AA', - palegreen: '#98FB98', - paleturquoise: '#AFEEEE', - palevioletred: '#DB7093', - papayawhip: '#FFEFD5', - peachpuff: '#FFDAB9', - peru: '#CD853F', - pink: '#FFC0CB', - plum: '#DDA0DD', - powderblue: '#B0E0E6', - rosybrown: '#BC8F8F', - royalblue: '#4169E1', - saddlebrown: '#8B4513', - salmon: '#FA8072', - sandybrown: '#F4A460', - seagreen: '#2E8B57', - seashell: '#FFF5EE', - sienna: '#A0522D', - skyblue: '#87CEEB', - slateblue: '#6A5ACD', - slategray: '#708090', - slategrey: '#708090', - snow: '#FFFAFA', - springgreen: '#00FF7F', - steelblue: '#4682B4', - tan: '#D2B48C', - thistle: '#D8BFD8', - tomato: '#FF6347', - turquoise: '#40E0D0', - violet: '#EE82EE', - wheat: '#F5DEB3', - whitesmoke: '#F5F5F5', - yellowgreen: '#9ACD32' - }; - - - function getRgbHslContent(styleString) { - var start = styleString.indexOf('(', 3); - var end = styleString.indexOf(')', start + 1); - var parts = styleString.substring(start + 1, end).split(','); - // add alpha if needed - if (parts.length != 4 || styleString.charAt(3) != 'a') { - parts[3] = 1; - } - return parts; - } - - function percent(s) { - return parseFloat(s) / 100; - } - - function clamp(v, min, max) { - return Math.min(max, Math.max(min, v)); - } - - function hslToRgb(parts){ - var r, g, b, h, s, l; - h = parseFloat(parts[0]) / 360 % 360; - if (h < 0) - h++; - s = clamp(percent(parts[1]), 0, 1); - l = clamp(percent(parts[2]), 0, 1); - if (s == 0) { - r = g = b = l; // achromatic - } else { - var q = l < 0.5 ? l * (1 + s) : l + s - l * s; - var p = 2 * l - q; - r = hueToRgb(p, q, h + 1 / 3); - g = hueToRgb(p, q, h); - b = hueToRgb(p, q, h - 1 / 3); - } - - return '#' + decToHex[Math.floor(r * 255)] + - decToHex[Math.floor(g * 255)] + - decToHex[Math.floor(b * 255)]; - } - - function hueToRgb(m1, m2, h) { - if (h < 0) - h++; - if (h > 1) - h--; - - if (6 * h < 1) - return m1 + (m2 - m1) * 6 * h; - else if (2 * h < 1) - return m2; - else if (3 * h < 2) - return m1 + (m2 - m1) * (2 / 3 - h) * 6; - else - return m1; - } - - var processStyleCache = {}; - - function processStyle(styleString) { - if (styleString in processStyleCache) { - return processStyleCache[styleString]; - } - - var str, alpha = 1; - - styleString = String(styleString); - if (styleString.charAt(0) == '#') { - str = styleString; - } else if (/^rgb/.test(styleString)) { - var parts = getRgbHslContent(styleString); - var str = '#', n; - for (var i = 0; i < 3; i++) { - if (parts[i].indexOf('%') != -1) { - n = Math.floor(percent(parts[i]) * 255); - } else { - n = +parts[i]; - } - str += decToHex[clamp(n, 0, 255)]; - } - alpha = +parts[3]; - } else if (/^hsl/.test(styleString)) { - var parts = getRgbHslContent(styleString); - str = hslToRgb(parts); - alpha = parts[3]; - } else { - str = colorData[styleString] || styleString; - } - return processStyleCache[styleString] = {color: str, alpha: alpha}; - } - - var DEFAULT_STYLE = { - style: 'normal', - variant: 'normal', - weight: 'normal', - size: 10, - family: 'sans-serif' - }; - - // Internal text style cache - var fontStyleCache = {}; - - function processFontStyle(styleString) { - if (fontStyleCache[styleString]) { - return fontStyleCache[styleString]; - } - - var el = document.createElement('div'); - var style = el.style; - try { - style.font = styleString; - } catch (ex) { - // Ignore failures to set to invalid font. - } - - return fontStyleCache[styleString] = { - style: style.fontStyle || DEFAULT_STYLE.style, - variant: style.fontVariant || DEFAULT_STYLE.variant, - weight: style.fontWeight || DEFAULT_STYLE.weight, - size: style.fontSize || DEFAULT_STYLE.size, - family: style.fontFamily || DEFAULT_STYLE.family - }; - } - - function getComputedStyle(style, element) { - var computedStyle = {}; - - for (var p in style) { - computedStyle[p] = style[p]; - } - - // Compute the size - var canvasFontSize = parseFloat(element.currentStyle.fontSize), - fontSize = parseFloat(style.size); - - if (typeof style.size == 'number') { - computedStyle.size = style.size; - } else if (style.size.indexOf('px') != -1) { - computedStyle.size = fontSize; - } else if (style.size.indexOf('em') != -1) { - computedStyle.size = canvasFontSize * fontSize; - } else if(style.size.indexOf('%') != -1) { - computedStyle.size = (canvasFontSize / 100) * fontSize; - } else if (style.size.indexOf('pt') != -1) { - computedStyle.size = fontSize / .75; - } else { - computedStyle.size = canvasFontSize; - } - - // Different scaling between normal text and VML text. This was found using - // trial and error to get the same size as non VML text. - computedStyle.size *= 0.981; - - return computedStyle; - } - - function buildStyle(style) { - return style.style + ' ' + style.variant + ' ' + style.weight + ' ' + - style.size + 'px ' + style.family; - } - - var lineCapMap = { - 'butt': 'flat', - 'round': 'round' - }; - - function processLineCap(lineCap) { - return lineCapMap[lineCap] || 'square'; - } - - /** - * This class implements CanvasRenderingContext2D interface as described by - * the WHATWG. - * @param {HTMLElement} canvasElement The element that the 2D context should - * be associated with - */ - function CanvasRenderingContext2D_(canvasElement) { - this.m_ = createMatrixIdentity(); - - this.mStack_ = []; - this.aStack_ = []; - this.currentPath_ = []; - - // Canvas context properties - this.strokeStyle = '#000'; - this.fillStyle = '#000'; - - this.lineWidth = 1; - this.lineJoin = 'miter'; - this.lineCap = 'butt'; - this.miterLimit = Z * 1; - this.globalAlpha = 1; - this.font = '10px sans-serif'; - this.textAlign = 'left'; - this.textBaseline = 'alphabetic'; - this.canvas = canvasElement; - - var cssText = 'width:' + canvasElement.clientWidth + 'px;height:' + - canvasElement.clientHeight + 'px;overflow:hidden;position:absolute'; - var el = canvasElement.ownerDocument.createElement('div'); - el.style.cssText = cssText; - canvasElement.appendChild(el); - - var overlayEl = el.cloneNode(false); - // Use a non transparent background. - overlayEl.style.backgroundColor = 'red'; - overlayEl.style.filter = 'alpha(opacity=0)'; - canvasElement.appendChild(overlayEl); - - this.element_ = el; - this.arcScaleX_ = 1; - this.arcScaleY_ = 1; - this.lineScale_ = 1; - } - - var contextPrototype = CanvasRenderingContext2D_.prototype; - contextPrototype.clearRect = function() { - if (this.textMeasureEl_) { - this.textMeasureEl_.removeNode(true); - this.textMeasureEl_ = null; - } - this.element_.innerHTML = ''; - }; - - contextPrototype.beginPath = function() { - // TODO: Branch current matrix so that save/restore has no effect - // as per safari docs. - this.currentPath_ = []; - }; - - contextPrototype.moveTo = function(aX, aY) { - var p = getCoords(this, aX, aY); - this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y}); - this.currentX_ = p.x; - this.currentY_ = p.y; - }; - - contextPrototype.lineTo = function(aX, aY) { - var p = getCoords(this, aX, aY); - this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y}); - - this.currentX_ = p.x; - this.currentY_ = p.y; - }; - - contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, - aCP2x, aCP2y, - aX, aY) { - var p = getCoords(this, aX, aY); - var cp1 = getCoords(this, aCP1x, aCP1y); - var cp2 = getCoords(this, aCP2x, aCP2y); - bezierCurveTo(this, cp1, cp2, p); - }; - - // Helper function that takes the already fixed cordinates. - function bezierCurveTo(self, cp1, cp2, p) { - self.currentPath_.push({ - type: 'bezierCurveTo', - cp1x: cp1.x, - cp1y: cp1.y, - cp2x: cp2.x, - cp2y: cp2.y, - x: p.x, - y: p.y - }); - self.currentX_ = p.x; - self.currentY_ = p.y; - } - - contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) { - // the following is lifted almost directly from - // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes - - var cp = getCoords(this, aCPx, aCPy); - var p = getCoords(this, aX, aY); - - var cp1 = { - x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_), - y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_) - }; - var cp2 = { - x: cp1.x + (p.x - this.currentX_) / 3.0, - y: cp1.y + (p.y - this.currentY_) / 3.0 - }; - - bezierCurveTo(this, cp1, cp2, p); - }; - - contextPrototype.arc = function(aX, aY, aRadius, - aStartAngle, aEndAngle, aClockwise) { - aRadius *= Z; - var arcType = aClockwise ? 'at' : 'wa'; - - var xStart = aX + mc(aStartAngle) * aRadius - Z2; - var yStart = aY + ms(aStartAngle) * aRadius - Z2; - - var xEnd = aX + mc(aEndAngle) * aRadius - Z2; - var yEnd = aY + ms(aEndAngle) * aRadius - Z2; - - // IE won't render arches drawn counter clockwise if xStart == xEnd. - if (xStart == xEnd && !aClockwise) { - xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something - // that can be represented in binary - } - - var p = getCoords(this, aX, aY); - var pStart = getCoords(this, xStart, yStart); - var pEnd = getCoords(this, xEnd, yEnd); - - this.currentPath_.push({type: arcType, - x: p.x, - y: p.y, - radius: aRadius, - xStart: pStart.x, - yStart: pStart.y, - xEnd: pEnd.x, - yEnd: pEnd.y}); - - }; - - contextPrototype.rect = function(aX, aY, aWidth, aHeight) { - this.moveTo(aX, aY); - this.lineTo(aX + aWidth, aY); - this.lineTo(aX + aWidth, aY + aHeight); - this.lineTo(aX, aY + aHeight); - this.closePath(); - }; - - contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) { - var oldPath = this.currentPath_; - this.beginPath(); - - this.moveTo(aX, aY); - this.lineTo(aX + aWidth, aY); - this.lineTo(aX + aWidth, aY + aHeight); - this.lineTo(aX, aY + aHeight); - this.closePath(); - this.stroke(); - - this.currentPath_ = oldPath; - }; - - contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) { - var oldPath = this.currentPath_; - this.beginPath(); - - this.moveTo(aX, aY); - this.lineTo(aX + aWidth, aY); - this.lineTo(aX + aWidth, aY + aHeight); - this.lineTo(aX, aY + aHeight); - this.closePath(); - this.fill(); - - this.currentPath_ = oldPath; - }; - - contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) { - var gradient = new CanvasGradient_('gradient'); - gradient.x0_ = aX0; - gradient.y0_ = aY0; - gradient.x1_ = aX1; - gradient.y1_ = aY1; - return gradient; - }; - - contextPrototype.createRadialGradient = function(aX0, aY0, aR0, - aX1, aY1, aR1) { - var gradient = new CanvasGradient_('gradientradial'); - gradient.x0_ = aX0; - gradient.y0_ = aY0; - gradient.r0_ = aR0; - gradient.x1_ = aX1; - gradient.y1_ = aY1; - gradient.r1_ = aR1; - return gradient; - }; - - contextPrototype.drawImage = function(image, var_args) { - var dx, dy, dw, dh, sx, sy, sw, sh; - - // to find the original width we overide the width and height - var oldRuntimeWidth = image.runtimeStyle.width; - var oldRuntimeHeight = image.runtimeStyle.height; - image.runtimeStyle.width = 'auto'; - image.runtimeStyle.height = 'auto'; - - // get the original size - var w = image.width; - var h = image.height; - - // and remove overides - image.runtimeStyle.width = oldRuntimeWidth; - image.runtimeStyle.height = oldRuntimeHeight; - - if (arguments.length == 3) { - dx = arguments[1]; - dy = arguments[2]; - sx = sy = 0; - sw = dw = w; - sh = dh = h; - } else if (arguments.length == 5) { - dx = arguments[1]; - dy = arguments[2]; - dw = arguments[3]; - dh = arguments[4]; - sx = sy = 0; - sw = w; - sh = h; - } else if (arguments.length == 9) { - sx = arguments[1]; - sy = arguments[2]; - sw = arguments[3]; - sh = arguments[4]; - dx = arguments[5]; - dy = arguments[6]; - dw = arguments[7]; - dh = arguments[8]; - } else { - throw Error('Invalid number of arguments'); - } - - var d = getCoords(this, dx, dy); - - var w2 = sw / 2; - var h2 = sh / 2; - - var vmlStr = []; - - var W = 10; - var H = 10; - - // For some reason that I've now forgotten, using divs didn't work - vmlStr.push(' ' , - '', - ''); - - this.element_.insertAdjacentHTML('BeforeEnd', vmlStr.join('')); - }; - - contextPrototype.stroke = function(aFill) { - var lineStr = []; - var lineOpen = false; - - var W = 10; - var H = 10; - - lineStr.push(''); - - if (!aFill) { - appendStroke(this, lineStr); - } else { - appendFill(this, lineStr, min, max); - } - - lineStr.push(''); - - this.element_.insertAdjacentHTML('beforeEnd', lineStr.join('')); - }; - - function appendStroke(ctx, lineStr) { - var a = processStyle(ctx.strokeStyle); - var color = a.color; - var opacity = a.alpha * ctx.globalAlpha; - var lineWidth = ctx.lineScale_ * ctx.lineWidth; - - // VML cannot correctly render a line if the width is less than 1px. - // In that case, we dilute the color to make the line look thinner. - if (lineWidth < 1) { - opacity *= lineWidth; - } - - lineStr.push( - '' - ); - } - - function appendFill(ctx, lineStr, min, max) { - var fillStyle = ctx.fillStyle; - var arcScaleX = ctx.arcScaleX_; - var arcScaleY = ctx.arcScaleY_; - var width = max.x - min.x; - var height = max.y - min.y; - if (fillStyle instanceof CanvasGradient_) { - // TODO: Gradients transformed with the transformation matrix. - var angle = 0; - var focus = {x: 0, y: 0}; - - // additional offset - var shift = 0; - // scale factor for offset - var expansion = 1; - - if (fillStyle.type_ == 'gradient') { - var x0 = fillStyle.x0_ / arcScaleX; - var y0 = fillStyle.y0_ / arcScaleY; - var x1 = fillStyle.x1_ / arcScaleX; - var y1 = fillStyle.y1_ / arcScaleY; - var p0 = getCoords(ctx, x0, y0); - var p1 = getCoords(ctx, x1, y1); - var dx = p1.x - p0.x; - var dy = p1.y - p0.y; - angle = Math.atan2(dx, dy) * 180 / Math.PI; - - // The angle should be a non-negative number. - if (angle < 0) { - angle += 360; - } - - // Very small angles produce an unexpected result because they are - // converted to a scientific notation string. - if (angle < 1e-6) { - angle = 0; - } - } else { - var p0 = getCoords(ctx, fillStyle.x0_, fillStyle.y0_); - focus = { - x: (p0.x - min.x) / width, - y: (p0.y - min.y) / height - }; - - width /= arcScaleX * Z; - height /= arcScaleY * Z; - var dimension = m.max(width, height); - shift = 2 * fillStyle.r0_ / dimension; - expansion = 2 * fillStyle.r1_ / dimension - shift; - } - - // We need to sort the color stops in ascending order by offset, - // otherwise IE won't interpret it correctly. - var stops = fillStyle.colors_; - stops.sort(function(cs1, cs2) { - return cs1.offset - cs2.offset; - }); - - var length = stops.length; - var color1 = stops[0].color; - var color2 = stops[length - 1].color; - var opacity1 = stops[0].alpha * ctx.globalAlpha; - var opacity2 = stops[length - 1].alpha * ctx.globalAlpha; - - var colors = []; - for (var i = 0; i < length; i++) { - var stop = stops[i]; - colors.push(stop.offset * expansion + shift + ' ' + stop.color); - } - - // When colors attribute is used, the meanings of opacity and o:opacity2 - // are reversed. - lineStr.push(''); - } else if (fillStyle instanceof CanvasPattern_) { - if (width && height) { - var deltaLeft = -min.x; - var deltaTop = -min.y; - lineStr.push(''); - } - } else { - var a = processStyle(ctx.fillStyle); - var color = a.color; - var opacity = a.alpha * ctx.globalAlpha; - lineStr.push(''); - } - } - - contextPrototype.fill = function() { - this.stroke(true); - }; - - contextPrototype.closePath = function() { - this.currentPath_.push({type: 'close'}); - }; - - function getCoords(ctx, aX, aY) { - var m = ctx.m_; - return { - x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2, - y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2 - }; - }; - - contextPrototype.save = function() { - var o = {}; - copyState(this, o); - this.aStack_.push(o); - this.mStack_.push(this.m_); - this.m_ = matrixMultiply(createMatrixIdentity(), this.m_); - }; - - contextPrototype.restore = function() { - if (this.aStack_.length) { - copyState(this.aStack_.pop(), this); - this.m_ = this.mStack_.pop(); - } - }; - - function matrixIsFinite(m) { - return isFinite(m[0][0]) && isFinite(m[0][1]) && - isFinite(m[1][0]) && isFinite(m[1][1]) && - isFinite(m[2][0]) && isFinite(m[2][1]); - } - - function setM(ctx, m, updateLineScale) { - if (!matrixIsFinite(m)) { - return; - } - ctx.m_ = m; - - if (updateLineScale) { - // Get the line scale. - // Determinant of this.m_ means how much the area is enlarged by the - // transformation. So its square root can be used as a scale factor - // for width. - var det = m[0][0] * m[1][1] - m[0][1] * m[1][0]; - ctx.lineScale_ = sqrt(abs(det)); - } - } - - contextPrototype.translate = function(aX, aY) { - var m1 = [ - [1, 0, 0], - [0, 1, 0], - [aX, aY, 1] - ]; - - setM(this, matrixMultiply(m1, this.m_), false); - }; - - contextPrototype.rotate = function(aRot) { - var c = mc(aRot); - var s = ms(aRot); - - var m1 = [ - [c, s, 0], - [-s, c, 0], - [0, 0, 1] - ]; - - setM(this, matrixMultiply(m1, this.m_), false); - }; - - contextPrototype.scale = function(aX, aY) { - this.arcScaleX_ *= aX; - this.arcScaleY_ *= aY; - var m1 = [ - [aX, 0, 0], - [0, aY, 0], - [0, 0, 1] - ]; - - setM(this, matrixMultiply(m1, this.m_), true); - }; - - contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) { - var m1 = [ - [m11, m12, 0], - [m21, m22, 0], - [dx, dy, 1] - ]; - - setM(this, matrixMultiply(m1, this.m_), true); - }; - - contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) { - var m = [ - [m11, m12, 0], - [m21, m22, 0], - [dx, dy, 1] - ]; - - setM(this, m, true); - }; - - /** - * The text drawing function. - * The maxWidth argument isn't taken in account, since no browser supports - * it yet. - */ - contextPrototype.drawText_ = function(text, x, y, maxWidth, stroke) { - var m = this.m_, - delta = 1000, - left = 0, - right = delta, - offset = {x: 0, y: 0}, - lineStr = []; - - var fontStyle = getComputedStyle(processFontStyle(this.font), - this.element_); - - var fontStyleString = buildStyle(fontStyle); - - var elementStyle = this.element_.currentStyle; - var textAlign = this.textAlign.toLowerCase(); - switch (textAlign) { - case 'left': - case 'center': - case 'right': - break; - case 'end': - textAlign = elementStyle.direction == 'ltr' ? 'right' : 'left'; - break; - case 'start': - textAlign = elementStyle.direction == 'rtl' ? 'right' : 'left'; - break; - default: - textAlign = 'left'; - } - - // 1.75 is an arbitrary number, as there is no info about the text baseline - switch (this.textBaseline) { - case 'hanging': - case 'top': - offset.y = fontStyle.size / 1.75; - break; - case 'middle': - break; - default: - case null: - case 'alphabetic': - case 'ideographic': - case 'bottom': - offset.y = -fontStyle.size / 2.25; - break; - } - - switch(textAlign) { - case 'right': - left = delta; - right = 0.05; - break; - case 'center': - left = right = delta / 2; - break; - } - - var d = getCoords(this, x + offset.x, y + offset.y); - - lineStr.push(''); - - if (stroke) { - appendStroke(this, lineStr); - } else { - // TODO: Fix the min and max params. - appendFill(this, lineStr, {x: -left, y: 0}, - {x: right, y: fontStyle.size}); - } - - var skewM = m[0][0].toFixed(3) + ',' + m[1][0].toFixed(3) + ',' + - m[0][1].toFixed(3) + ',' + m[1][1].toFixed(3) + ',0,0'; - - var skewOffset = mr(d.x / Z) + ',' + mr(d.y / Z); - - lineStr.push('', - '', - ''); - - this.element_.insertAdjacentHTML('beforeEnd', lineStr.join('')); - }; - - contextPrototype.fillText = function(text, x, y, maxWidth) { - this.drawText_(text, x, y, maxWidth, false); - }; - - contextPrototype.strokeText = function(text, x, y, maxWidth) { - this.drawText_(text, x, y, maxWidth, true); - }; - - contextPrototype.measureText = function(text) { - if (!this.textMeasureEl_) { - var s = ''; - this.element_.insertAdjacentHTML('beforeEnd', s); - this.textMeasureEl_ = this.element_.lastChild; - } - var doc = this.element_.ownerDocument; - this.textMeasureEl_.innerHTML = ''; - this.textMeasureEl_.style.font = this.font; - // Don't use innerHTML or innerText because they allow markup/whitespace. - this.textMeasureEl_.appendChild(doc.createTextNode(text)); - return {width: this.textMeasureEl_.offsetWidth}; - }; - - /******** STUBS ********/ - contextPrototype.clip = function() { - // TODO: Implement - }; - - contextPrototype.arcTo = function() { - // TODO: Implement - }; - - contextPrototype.createPattern = function(image, repetition) { - return new CanvasPattern_(image, repetition); - }; - - // Gradient / Pattern Stubs - function CanvasGradient_(aType) { - this.type_ = aType; - this.x0_ = 0; - this.y0_ = 0; - this.r0_ = 0; - this.x1_ = 0; - this.y1_ = 0; - this.r1_ = 0; - this.colors_ = []; - } - - CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) { - aColor = processStyle(aColor); - this.colors_.push({offset: aOffset, - color: aColor.color, - alpha: aColor.alpha}); - }; - - function CanvasPattern_(image, repetition) { - assertImageIsValid(image); - switch (repetition) { - case 'repeat': - case null: - case '': - this.repetition_ = 'repeat'; - break - case 'repeat-x': - case 'repeat-y': - case 'no-repeat': - this.repetition_ = repetition; - break; - default: - throwException('SYNTAX_ERR'); - } - - this.src_ = image.src; - this.width_ = image.width; - this.height_ = image.height; - } - - function throwException(s) { - throw new DOMException_(s); - } - - function assertImageIsValid(img) { - if (!img || img.nodeType != 1 || img.tagName != 'IMG') { - throwException('TYPE_MISMATCH_ERR'); - } - if (img.readyState != 'complete') { - throwException('INVALID_STATE_ERR'); - } - } - - function DOMException_(s) { - this.code = this[s]; - this.message = s +': DOM Exception ' + this.code; - } - var p = DOMException_.prototype = new Error; - p.INDEX_SIZE_ERR = 1; - p.DOMSTRING_SIZE_ERR = 2; - p.HIERARCHY_REQUEST_ERR = 3; - p.WRONG_DOCUMENT_ERR = 4; - p.INVALID_CHARACTER_ERR = 5; - p.NO_DATA_ALLOWED_ERR = 6; - p.NO_MODIFICATION_ALLOWED_ERR = 7; - p.NOT_FOUND_ERR = 8; - p.NOT_SUPPORTED_ERR = 9; - p.INUSE_ATTRIBUTE_ERR = 10; - p.INVALID_STATE_ERR = 11; - p.SYNTAX_ERR = 12; - p.INVALID_MODIFICATION_ERR = 13; - p.NAMESPACE_ERR = 14; - p.INVALID_ACCESS_ERR = 15; - p.VALIDATION_ERR = 16; - p.TYPE_MISMATCH_ERR = 17; - - // set up externs - G_vmlCanvasManager = G_vmlCanvasManager_; - CanvasRenderingContext2D = CanvasRenderingContext2D_; - CanvasGradient = CanvasGradient_; - CanvasPattern = CanvasPattern_; - DOMException = DOMException_; -})(); - -} // if \ No newline at end of file diff --git a/site/assets/html.png b/site/assets/html.png deleted file mode 100644 index 55439dc9a6bb8f7b5d4791e5c84f28607396f7ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83653 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™^8ykZ5u@N1ig0(e@!@lB$1> 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
-
-
-
-
-

Chart.js

-

Easy, object oriented client side graphs for designers and developers

-
- - - -
DocumentationDownload -
-
-
- -

6 Chart types

-

Visualise your data in different ways. Each of them animated, fully customisable and look great, even on retina displays.

-
-
- -

HTML5 Based

-

Chart.js uses the HTML5 canvas element. It supports all modern browsers, and polyfills provide support for IE7/8.

-
-
- -

Simple and flexible

-

Chart.js is dependency free, lightweight (4.5k when minified and gzipped) and offers loads of customisation options.

-
-
-
- - - - - - - - - - - -

Like what you see? Download Chart.js on Github or read detailed documentation

-
- - - - - diff --git a/site/styles.css b/site/styles.css deleted file mode 100644 index e24d05dca59..00000000000 --- a/site/styles.css +++ /dev/null @@ -1,205 +0,0 @@ -* { - padding: 0; - margin: 0; - color: inherit; - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; -} -body { - color: #282b36; - border-top: 8px solid #282b36; -} -canvas { - font-family: "proxima-nova", sans-serif sans-serif; -} -.redBorder, -.greenBorder, -.yellowBorder { - width: 33.33%; - float: left; - height: 8px; -} -.redBorder { - background-color: #f33e6f; -} -.greenBorder { - background-color: #46bfbd; -} -.yellowBorder { - background-color: #fdb45c; -} -h1 { - font-family: "proxima-nova", sans-serif; - font-weight: 600; - font-size: 55px; - margin-top: 40px; -} -h2 { - font-family: "proxima-nova", sans-serif; - font-weight: 400; - margin-top: 20px; - font-size: 26px; - line-height: 40px; -} -h3 { - font-family: "proxima-nova", sans-serif; - font-weight: 600; - text-align: center; - margin: 20px 0; -} -h3 a { - color: #2d91ea; - text-decoration: none; - border-bottom: 1px solid #2d91ea; -} -p { - font-family: "proxima-nova", sans-serif; - line-height: 24px; - font-size: 18px; - color: #767c8d; -} -.btn { - display: inline-block; - padding: 20px; - font-family: "proxima-nova", sans-serif; - font-weight: 600; - color: #fff; - text-decoration: none; - border-radius: 5px; - text-align: center; - font-size: 18px; - -webkit-transition-property: background-color box-shadow; - -webkit-transition-duration: 200ms; - -webkit-transition-timing-function: ease-in-out; - -moz-transition-property: background-color box-shadow; - -moz-transition-duration: 200ms; - -moz-transition-timing-function: ease-in-out; - -ms-transition-property: background-color box-shadow; - -ms-transition-duration: 200ms; - -ms-transition-timing-function: ease-in-out; - -o-transition-property: background-color box-shadow; - -o-transition-duration: 200ms; - -o-transition-timing-function: ease-in-out; - transition-property: background-color box-shadow; - transition-duration: 200ms; - transition-timing-function: ease-in-out; -} -.btn:active { - box-shadow: inset 1px 1px 4px rgba(0, 0, 0, 0.25); -} -.btn.red { - background-color: #f33e6f; -} -.btn.red:hover { - background-color: #f2265d; -} -.btn.blue { - background-color: #2d91ea; -} -.btn.blue:hover { - background-color: #1785e6; -} -header { - width: 978px; - margin: 20px auto; - display: block; - position: relative; -} -header hgroup { - width: 50%; - padding: 20px 0; -} -header #introChart { - position: absolute; - top: 60px; - right: 0; -} -header .btn { - margin-right: 10px; - width: 180px; -} -footer { - width: 100%; - text-align: center; - background-color: #ebebeb; -} -footer p { - color: #767c8d; - font-family: "proxima-nova", sans-serif; - font-size: 16px; - padding: 20px 0; -} -section { - width: 978px; - margin: 40px auto; -} -section:before { - content: ''; - width: 600px; - margin: 0 auto; - border-top: 1px solid #ebebeb; - height: 20px; - display: block; -} -section:after { - content: ""; - display: table; - clear: both; -} -*section { - zoom: 1; -} -#features article { - width: 33.33%; - float: left; -} -#features article p { - display: block; - width: 90%; - margin: 0 auto; - text-align: center; -} -#features article img { - width: 250px; - height: 250px; - margin: 0 auto; - display: block; -} -#examples article { - -webkit-transition: opacity 200ms ease-in-out; - -ms-transition: opacity 200ms ease-in-out; - -moz-transition: opacity 200ms ease-in-out; - -o-transition: opacity 200ms ease-in-out; - transition: opacity 200ms ease-in-out; - position: relative; - margin-top: 20px; - clear: both; -} -#examples article:after { - content: ''; - width: 600px; - padding-top: 40px; - margin: 40px auto; - border-bottom: 1px solid #ebebeb; - height: 20px; - display: block; - clear: both; -} -#examples article p { - margin-top: 10px; -} -#examples article .half { - width: 50%; - float: left; -} -#examples article .canvasWrapper { - float: left; - width: 449px; - padding: 0 20px; -} -#examples h3 { - clear: both; -} -#examples .hidden { - opacity: 0; -} From dc6d5bc934f582d32abb89abc506738d80e29548 Mon Sep 17 00:00:00 2001 From: Nick Downie Date: Sun, 29 Jun 2014 18:35:49 +0100 Subject: [PATCH 4/9] Add new docs as markdown files --- docs/00-Getting-Started.md | 197 ++++++++++++++++++++++++++++++++++ docs/01-Line-Chart.md | 160 +++++++++++++++++++++++++++ docs/02-Bar-Chart.md | 143 ++++++++++++++++++++++++ docs/03-Radar-Chart.md | 177 ++++++++++++++++++++++++++++++ docs/04-Polar-Area-Chart.md | 172 +++++++++++++++++++++++++++++ docs/05-Pie-Doughnut-Chart.md | 158 +++++++++++++++++++++++++++ docs/06-Advanced.md | 144 +++++++++++++++++++++++++ docs/07-Notes.md | 42 ++++++++ 8 files changed, 1193 insertions(+) create mode 100644 docs/00-Getting-Started.md create mode 100644 docs/01-Line-Chart.md create mode 100644 docs/02-Bar-Chart.md create mode 100644 docs/03-Radar-Chart.md create mode 100644 docs/04-Polar-Area-Chart.md create mode 100644 docs/05-Pie-Doughnut-Chart.md create mode 100644 docs/06-Advanced.md create mode 100644 docs/07-Notes.md diff --git a/docs/00-Getting-Started.md b/docs/00-Getting-Started.md new file mode 100644 index 00000000000..414a7d4f4ff --- /dev/null +++ b/docs/00-Getting-Started.md @@ -0,0 +1,197 @@ +--- +title: Getting started +anchor: getting-started +--- + +###Include Chart.js + +First we need to include the Chart.js library on the page. The library occupies a global variable of `Chart`. + +```html + +``` + +Alternatively, if you're using an AMD loader for javascript modules, that is also supported in the Chart.js core. Please note: the library will still occupy a global variable of `Chart`, even if it detects `define` and `define.amd`. If this is a problem, you can call `noConflict` to restore the global Chart variable to it's previous owner. + +```javascript +// Using requirejs +require(['path/to/Chartjs'], function(Chart){ + // Use Chart.js as normal here. + + // Chart.noConflict restores the Chart global variable to it's previous owner + // The function returns what was previously Chart, allowing you to reassign. + var Chartjs = Chart.noConflict(); + +}); +``` + +You can also grab Chart.js using bower: + +```bash +bower install chartjs --save +``` + +###Creating a chart + +To create a chart, we need to instantiate the `Chart` class. To do this, we need to pass in the 2d context of where we want to draw the chart. Here's an example. + +```html + +``` + +```javascript +// Get the context of the canvas element we want to select +var ctx = document.getElementById("myChart").getContext("2d"); +var myNewChart = new Chart(ctx).PolarArea(data); +``` + +We can also get the context of our canvas with jQuery. To do this, we need to get the DOM node out of the jQuery collection, and call the `getContext("2d")` method on that. + +```javascript +// Get context with jQuery - using jQuery's .get() method. +var ctx = $("#myChart").get(0).getContext("2d"); +// This will get the first returned node in the jQuery collection. +var myNewChart = new Chart(ctx); +``` + +After we've instantiated the Chart class on the canvas we want to draw on, Chart.js will handle the scaling for retina displays. + +With the Chart class set up, we can go on to create one of the charts Chart.js has available. In the example below, we would be drawing a Polar area chart. + +```javascript +new Chart(ctx).PolarArea(data, options); +``` + +We call a method of the name of the chart we want to create. We pass in the data for that chart type, and the options for that chart as parameters. Chart.js will merge the global defaults with chart type specific defaults, then merge any options passed in as a second argument after data. + +###Global chart configuration + +This concept was introduced in Chart.js 1.0 to keep configuration DRY, and allow for changing options globally across chart types, avoiding the need to specify options for each instance, or the default for a particular chart type. + +```javascript +Chart.defaults.global = { + // Boolean - Whether to animate the chart + animation: true, + + // Number - Number of animation steps + animationSteps: 60, + + // String - Animation easing effect + animationEasing: "easeOutQuart", + + // Boolean - If we should show the scale at all + showScale: true, + + // Boolean - If we want to override with a hard coded scale + scaleOverride: false, + + // ** Required if scaleOverride is true ** + // Number - The number of steps in a hard coded scale + scaleSteps: null, + // Number - The value jump in the hard coded scale + scaleStepWidth: null, + // Number - The scale starting value + scaleStartValue: null, + + // String - Colour of the scale line + scaleLineColor: "rgba(0,0,0,.1)", + + // Number - Pixel width of the scale line + scaleLineWidth: 1, + + // Boolean - Whether to show labels on the scale + scaleShowLabels: true, + + // Interpolated JS string - can access value + scaleLabel: "<%=value%>", + + // Boolean - Whether the scale should stick to integers, not floats even if drawing space is there + scaleIntegersOnly: true, + + // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero: false, + + // String - Scale label font declaration for the scale label + scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Scale label font size in pixels + scaleFontSize: 12, + + // String - Scale label font weight style + scaleFontStyle: "normal", + + // String - Scale label font colour + scaleFontColor: "#666", + + // Boolean - whether or not the chart should be responsive and resize when the browser does. + responsive: false, + + // Boolean - Determines whether to draw tooltips on the canvas or not + showTooltips: true, + + // Array - Array of string names to attach tooltip events + tooltipEvents: ["mousemove", "touchstart", "touchmove"], + + // String - Tooltip background colour + tooltipFillColor: "rgba(0,0,0,0.8)", + + // String - Tooltip label font declaration for the scale label + tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip label font size in pixels + tooltipFontSize: 14, + + // String - Tooltip font weight style + tooltipFontStyle: "normal", + + // String - Tooltip label font colour + tooltipFontColor: "#fff", + + // String - Tooltip title font declaration for the scale label + tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip title font size in pixels + tooltipTitleFontSize: 14, + + // String - Tooltip title font weight style + tooltipTitleFontStyle: "bold", + + // String - Tooltip title font colour + tooltipTitleFontColor: "#fff", + + // Number - pixel width of padding around tooltip text + tooltipYPadding: 6, + + // Number - pixel width of padding around tooltip text + tooltipXPadding: 6, + + // Number - Size of the caret on the tooltip + tooltipCaretSize: 8, + + // Number - Pixel radius of the tooltip border + tooltipCornerRadius: 6, + + // Number - Pixel offset from point x to tooltip edge + tooltipXOffset: 10, + {% raw %} + // String - Template string for single tooltips + tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", + {% endraw %} + // String - Template string for single tooltips + multiTooltipTemplate: "<%= value %>", + + // Function - Will fire on animation progression. + onAnimationProgress: function(){}, + + // Function - Will fire on animation completion. + onAnimationComplete: function(){} +} +``` + +If for example, you wanted all charts created to be responsive, and resize when the browser window does, the following setting can be changed: + +```javascript +Chart.defaults.global.responsive = true; +``` + +Now, every time we create a chart, `options.responsive` will be `true`. diff --git a/docs/01-Line-Chart.md b/docs/01-Line-Chart.md new file mode 100644 index 00000000000..3dc60633744 --- /dev/null +++ b/docs/01-Line-Chart.md @@ -0,0 +1,160 @@ +--- +title: Line Chart +anchor: line-chart +--- +###Introduction +A line chart is a way of plotting data points on a line. + +Often, it is used to show trend data, and the comparison of two data sets. + +
+ +
+ +###Example usage +```javascript +var myLineChart = new Chart(ctx).Line(data, options); +``` +###Data structure + +```javascript +var data = { + labels: ["January", "February", "March", "April", "May", "June", "July"], + datasets: [ + { + label: "My First dataset", + fillColor: "rgba(220,220,220,0.2)", + strokeColor: "rgba(220,220,220,1)", + pointColor: "rgba(220,220,220,1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(220,220,220,1)", + data: [65, 59, 80, 81, 56, 55, 40] + }, + { + label: "My Second dataset", + fillColor: "rgba(151,187,205,0.2)", + strokeColor: "rgba(151,187,205,1)", + pointColor: "rgba(151,187,205,1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(151,187,205,1)", + data: [28, 48, 40, 19, 86, 27, 90] + } + ] +}; +``` + +The line chart requires an array of labels for each of the data points. This is shown on the X axis. +The data for line charts is broken up into an array of datasets. Each dataset has a colour for the fill, a colour for the line and colours for the points and strokes of the points. These colours are strings just like CSS. You can use RGBA, RGB, HEX or HSL notation. + +The label key on each dataset is optional, and can be used when generating a scale for the chart. + +### Chart options + +These are the customisation options specific to Line charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. + +```javascript +{ + + ///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 the line is curved between points + bezierCurve : true, + + //Number - Tension of the bezier curve between points + bezierCurveTension : 0.4, + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 4, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + {% raw %} + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + {% endraw %} +}; +``` + +You can override these for your `Chart` instance by passing a second argument into the `Line` method as an object with the keys you want to override. + +For example, we could have a line chart without bezier curves between points by doing the following: + +```javascript +new Chart(ctx).Line(data, { + bezierCurve: false +}); +// This will create a chart with all of the default options, merged from the global config, +// and the Line chart defaults, but this particular instance will have `bezierCurve` set to false. +``` + +We can also change these defaults values for each Line type that is created, this object is available at `Chart.defaults.Line`. + + +### Prototype methods + +#### .getPointsAtEvent( event ) + +Calling `getPointsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the point elements that are at that the same position of that event. + +```javascript +canvas.onclick = function(evt){ + var activePoints = myLineChart.getPointsAtEvent(evt); + // => activePoints is an array of points on the canvas that are at the same position as the click event. +}; +``` + +This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. + +#### .update( ) + +Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. + +```javascript +myLineChart.datasets[0].points[2].value = 50; +// Would update the first dataset's value of 'March' to be 50 +myLineChart.update(); +// Calling update now animates the position of March from 90 to 50. +``` + +#### .addData( valuesArray, label ) + +Calling `addData(valuesArray, label)` on your Chart instance passing an array of values for each dataset, along with a label for those points. + +```javascript +// The values array passed into addData should be one for each dataset in the chart +myLineChart.addData([40, 60], "August"); +// This new data will now animate at the end of the chart. +``` + +#### .removeData( ) + +Calling `removeData()` on your Chart instance will remove the first value for all datasets on the chart. + +```javascript +myLineChart.removeData(); +// The chart will remove the first point and animate other points into place +``` diff --git a/docs/02-Bar-Chart.md b/docs/02-Bar-Chart.md new file mode 100644 index 00000000000..b4253e7463c --- /dev/null +++ b/docs/02-Bar-Chart.md @@ -0,0 +1,143 @@ +--- +title: Bar Chart +anchor: bar-chart +--- + +### Introduction +A bar chart is a way of showing data as bars. + +It is sometimes used to show trend data, and the comparison of multiple data sets side by side. + +
+ +
+ +### Example usage +```javascript +var myBarChart = new Chart(ctx).Bar(data, options); +``` + +### Data structure + +```javascript +var data = { + labels: ["January", "February", "March", "April", "May", "June", "July"], + datasets: [ + { + label: "My First dataset", + fillColor: "rgba(220,220,220,0.5)", + strokeColor: "rgba(220,220,220,0.8)", + highlightFill: "rgba(220,220,220,0.75)", + highlightStroke: "rgba(220,220,220,1)", + data: [65, 59, 80, 81, 56, 55, 40] + }, + { + label: "My Second dataset", + fillColor: "rgba(151,187,205,0.5)", + strokeColor: "rgba(151,187,205,0.8)", + highlightFill: "rgba(151,187,205,0.75)", + highlightStroke: "rgba(151,187,205,1)", + data: [28, 48, 40, 19, 86, 27, 90] + } + ] +}; +``` +The bar chart has the a very similar data structure to the line chart, and has an array of datasets, each with colours and an array of data. Again, colours are in CSS format. +We have an array of labels too for display. In the example, we are showing the same data as the previous line chart example. + +The label key on each dataset is optional, and can be used when generating a scale for the chart. + +### Chart Options + +These are the customisation options specific to Bar charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. + +```javascript +{ + //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 - 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, + {% raw %} + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + {% endraw %} +} +``` + +You can override these for your `Chart` instance by passing a second argument into the `Bar` method as an object with the keys you want to override. + +For example, we could have a bar chart without a stroke on each bar by doing the following: + +```javascript +new Chart(ctx).Bar(data, { + barShowStroke: false +}); +// This will create a chart with all of the default options, merged from the global config, +// and the Bar chart defaults but this particular instance will have `barShowStroke` set to false. +``` + +We can also change these defaults values for each Bar type that is created, this object is available at `Chart.defaults.Bar`. + +### Prototype methods + +#### .getBarsAtEvent( event ) + +Calling `getBarsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the bar elements that are at that the same position of that event. + +```javascript +canvas.onclick = function(evt){ + var activeBars = myBarChart.getBarsAtEvent(evt); + // => activeBars is an array of bars on the canvas that are at the same position as the click event. +}; +``` + +This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. + +#### .update( ) + +Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. + +```javascript +myBarChart.datasets[0].bars[2].value = 50; +// Would update the first dataset's value of 'March' to be 50 +myBarChart.update(); +// Calling update now animates the position of March from 90 to 50. +``` + +#### .addData( valuesArray, label ) + +Calling `addData(valuesArray, label)` on your Chart instance passing an array of values for each dataset, along with a label for those bars. + +```javascript +// The values array passed into addData should be one for each dataset in the chart +myBarChart.addData([40, 60], "August"); +// The new data will now animate at the end of the chart. +``` + +#### .removeData( ) + +Calling `removeData()` on your Chart instance will remove the first value for all datasets on the chart. + +```javascript +myBarChart.removeData(); +// The chart will now animate and remove the first bar +``` \ No newline at end of file diff --git a/docs/03-Radar-Chart.md b/docs/03-Radar-Chart.md new file mode 100644 index 00000000000..0707444b7b4 --- /dev/null +++ b/docs/03-Radar-Chart.md @@ -0,0 +1,177 @@ +--- +title: Radar Chart +anchor: radar-chart +--- + +###Introduction +A radar chart is a way of showing multiple data points and the variation between them. + +They are often useful for comparing the points of two or more different data sets. + +
+ +
+ +###Example usage + +```javascript +var myRadarChart = new Chart(ctx).Radar(data, options); +``` + +###Data structure +```javascript +var data = { + labels: ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"], + datasets: [ + { + label: "My First dataset", + fillColor: "rgba(220,220,220,0.2)", + strokeColor: "rgba(220,220,220,1)", + pointColor: "rgba(220,220,220,1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(220,220,220,1)", + data: [65, 59, 90, 81, 56, 55, 40] + }, + { + label: "My Second dataset", + fillColor: "rgba(151,187,205,0.2)", + strokeColor: "rgba(151,187,205,1)", + pointColor: "rgba(151,187,205,1)", + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: "rgba(151,187,205,1)", + data: [28, 48, 40, 19, 96, 27, 100] + } + ] +}; +``` +For a radar chart, to provide context of what each point means, we include an array of strings that show around each point in the chart. +For the radar chart data, we have an array of datasets. Each of these is an object, with a fill colour, a stroke colour, a colour for the fill of each point, and a colour for the stroke of each point. We also have an array of data values. + +The label key on each dataset is optional, and can be used when generating a scale for the chart. + +### Chart options + +These are the customisation options specific to Radar charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. + + +```javascript +{ + //Boolean - Whether to show lines for each scale point + scaleShowLine : true, + + //Boolean - Whether we show the angle lines out of the radar + angleShowLineOut : true, + + //Boolean - Whether to show labels on the scale + scaleShowLabels : false, + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //String - Colour of the angle line + angleLineColor : "rgba(0,0,0,.1)", + + //Number - Pixel width of the angle line + angleLineWidth : 1, + + //String - Point label font declaration + pointLabelFontFamily : "'Arial'", + + //String - Point label font weight + pointLabelFontStyle : "normal", + + //Number - Point label font size in pixels + pointLabelFontSize : 10, + + //String - Point label font colour + pointLabelFontColor : "#666", + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 3, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + {% raw %} + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + {% endraw %} +} +``` + + +You can override these for your `Chart` instance by passing a second argument into the `Radar` method as an object with the keys you want to override. + +For example, we could have a radar chart without a point for each on piece of data by doing the following: + +```javascript +new Chart(ctx).Radar(data, { + pointDot: false +}); +// This will create a chart with all of the default options, merged from the global config, +// and the Bar chart defaults but this particular instance will have `pointDot` set to false. +``` + +We can also change these defaults values for each Radar type that is created, this object is available at `Chart.defaults.Radar`. + + +### Prototype methods + +#### .getPointsAtEvent( event ) + +Calling `getPointsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the point elements that are at that the same position of that event. + +```javascript +canvas.onclick = function(evt){ + var activePoints = myRadarChart.getPointsAtEvent(evt); + // => activePoints is an array of points on the canvas that are at the same position as the click event. +}; +``` + +This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. + +#### .update( ) + +Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. + +```javascript +myRadarChart.datasets[0].points[2].value = 50; +// Would update the first dataset's value of 'Sleeping' to be 50 +myRadarChart.update(); +// Calling update now animates the position of Sleeping from 90 to 50. +``` + +#### .addData( valuesArray, label ) + +Calling `addData(valuesArray, label)` on your Chart instance passing an array of values for each dataset, along with a label for those points. + +```javascript +// The values array passed into addData should be one for each dataset in the chart +myRadarChart.addData([40, 60], "Dancing"); +// The new data will now animate at the end of the chart. +``` + +#### .removeData( ) + +Calling `removeData()` on your Chart instance will remove the first value for all datasets on the chart. + +```javascript +myRadarChart.removeData(); +// Other points will now animate to their correct positions. +``` \ No newline at end of file diff --git a/docs/04-Polar-Area-Chart.md b/docs/04-Polar-Area-Chart.md new file mode 100644 index 00000000000..658da54fcda --- /dev/null +++ b/docs/04-Polar-Area-Chart.md @@ -0,0 +1,172 @@ +--- +title: Polar Area Chart +anchor: polar-area-chart +--- +### Introduction +Polar area charts are similar to pie charts, but each segment has the same angle - the radius of the segment differs depending on the value. + +This type of chart is often useful when we want to show a comparison data similar to a pie chart, but also show a scale of values for context. + +
+ +
+ +### Example usage + +```javascript +new Chart(ctx).PolarArea(data, options); +``` + +### Data structure + +```javascript +var data = [ + { + value: 300, + color:"#F7464A", + highlight: "#FF5A5E", + label: "Red" + }, + { + value: 50, + color: "#46BFBD", + highlight: "#5AD3D1", + label: "Green" + }, + { + value: 100, + color: "#FDB45C", + highlight: "#FFC870", + label: "Yellow" + }, + { + value: 40, + color: "#949FB1", + highlight: "#A8B3C5", + label: "Grey" + }, + { + value: 120, + color: "#4D5360", + highlight: "#616774", + label: "Dark Grey" + } + +]; +``` +As you can see, for the chart data you pass in an array of objects, with a value and a colour. The value attribute should be a number, while the color attribute should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL. + +### Chart options + +These are the customisation options specific to Polar Area charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. + +```javascript +{ + //Boolean - Show a backdrop to the scale label + scaleShowLabelBackdrop : true, + + //String - The colour of the label backdrop + scaleBackdropColor : "rgba(255,255,255,0.75)", + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //Number - The backdrop padding above & below the label in pixels + scaleBackdropPaddingY : 2, + + //Number - The backdrop padding to the side of the label in pixels + scaleBackdropPaddingX : 2, + + //Boolean - Show line for each value in the scale + scaleShowLine : true, + + //Boolean - Stroke a line around each segment in the chart + segmentShowStroke : true, + + //String - The colour of the stroke on each segement. + segmentStrokeColor : "#fff", + + //Number - The width of the stroke value in pixels + segmentStrokeWidth : 2, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect. + animationEasing : "easeOutBounce", + + //Boolean - Whether to animate the rotation of the chart + animateRotate : true, + + //Boolean - Whether to animate scaling the chart from the centre + animateScale : false, + {% raw %} + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + {% endraw %} +} +``` + +You can override these for your `Chart` instance by passing a second argument into the `PolarArea` method as an object with the keys you want to override. + +For example, we could have a polar area chart with a black stroke on each segment like so: + +```javascript +new Chart(ctx).PolarArea(data, { + segmentStrokeColor: "#000000" +}); +// This will create a chart with all of the default options, merged from the global config, +// and the PolarArea chart defaults but this particular instance will have `segmentStrokeColor` set to `"#000000"`. +``` + +We can also change these defaults values for each PolarArea type that is created, this object is available at `Chart.defaults.PolarArea`. + +### Prototype methods + +#### .getSegmentsAtEvent( event ) + +Calling `getSegmentsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the segment elements that are at that the same position of that event. + +```javascript +canvas.onclick = function(evt){ + var activePoints = myPolarAreaChart.getSegmentsAtEvent(evt); + // => activePoints is an array of segments on the canvas that are at the same position as the click event. +}; +``` + +This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. + +#### .update( ) + +Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. + +```javascript +myPolarAreaChart.segments[1].value = 10; +// Would update the first dataset's value of 'Green' to be 10 +myPolarAreaChart.update(); +// Calling update now animates the position of Green from 50 to 10. +``` + +#### .addData( segmentData, index ) + +Calling `addData(segmentData, index)` on your Chart instance passing an object in the same format as in the constructor. There is an option second argument of 'index', this determines at what index the new segment should be inserted into the chart. + +```javascript +// An object in the same format as the original data source +myPolarAreaChart.addData({ + value: 130, + color: "#B48EAD", + highlight: "#C69CBE", + label: "Purple" +}); +// The new segment will now animate in. +``` + +#### .removeData( index ) + +Calling `removeData(index)` on your Chart instance will remove segment at that particular index. If none is provided, it will default to the last segment. + +```javascript +myPolarAreaChart.removeData(); +// Other segments will update to fill the empty space left. +``` \ No newline at end of file diff --git a/docs/05-Pie-Doughnut-Chart.md b/docs/05-Pie-Doughnut-Chart.md new file mode 100644 index 00000000000..9c2b8a777ce --- /dev/null +++ b/docs/05-Pie-Doughnut-Chart.md @@ -0,0 +1,158 @@ +--- +title: Pie & Doughnut Charts +anchor: doughnut-pie-chart +--- +###Introduction +Pie and doughnut charts are probably the most commonly used chart there are. They are divided into segments, the arc of each segment shows a the proportional value of each piece of data. + +They are excellent at showing the relational proportions between data. + +Pie and doughnut charts in are effectively the same class in Chart.js, but have one different default value - their `percentageInnerCutout`. This equates what percentage of the inner should be cut out. This defaults to `0` for pie charts, and `50` for doughnuts. + +They are also registered under two aliases in the `Chart` core. Other than their different default value, and different alias, they are exactly the same. + +
+ +
+ +
+ +
+ + +### Example usage + +```javascript +// For a pie chart +var myPieChart = new Chart(ctx[0]).Pie(data,options); + +// And for a doughnut chart +var myDoughnutChart = new Chart(ctx[1]).Doughnut(data,options); +``` + +### Data structure + +```javascript +var data = [ + { + value: 300, + color:"#F7464A", + highlight: "#FF5A5E", + label: "Red" + }, + { + value: 50, + color: "#46BFBD", + highlight: "#5AD3D1", + label: "Green" + }, + { + value: 100, + color: "#FDB45C", + highlight: "#FFC870", + label: "Yellow" + } +] +``` + +For a pie chart, you must pass in an array of objects with a value and a color property. The value attribute should be a number, Chart.js will total all of the numbers and calculate the relative proportion of each. The color attribute should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL. + +### Chart options + +These are the customisation options specific to Pie & Doughnut charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. + +```javascript +{ + //Boolean - Whether we should show a stroke on each segment + segmentShowStroke : true, + + //String - The colour of each segment stroke + segmentStrokeColor : "#fff", + + //Number - The width of each segment stroke + segmentStrokeWidth : 2, + + //Number - The percentage of the chart that we cut out of the middle + percentageInnerCutout : 50, // This is 0 for Pie charts + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect + animationEasing : "easeOutBounce", + + //Boolean - Whether we animate the rotation of the Doughnut + animateRotate : true, + + //Boolean - Whether we animate scaling the Doughnut from the centre + animateScale : false, + {% raw %} + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + {% endraw %} +} +``` +You can override these for your `Chart` instance by passing a second argument into the `Doughnut` method as an object with the keys you want to override. + +For example, we could have a doughnut chart that animates by scaling out from the centre like so: + +```javascript +new Chart(ctx).Doughnut(data, { + animateScale: true +}); +// This will create a chart with all of the default options, merged from the global config, +// and the Doughnut chart defaults but this particular instance will have `animateScale` set to `true`. +``` + +We can also change these defaults values for each Doughnut type that is created, this object is available at `Chart.defaults.Doughnut`. Pie charts also have a clone of these defaults available to change at `Chart.defaults.Pie`, with the only difference being `percentageInnerCutout` being set to 0. + +### Prototype methods + +#### .getSegmentsAtEvent( event ) + +Calling `getSegmentsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the segment elements that are at that the same position of that event. + +```javascript +canvas.onclick = function(evt){ + var activePoints = myDoughnutChart.getSegmentsAtEvent(evt); + // => activePoints is an array of segments on the canvas that are at the same position as the click event. +}; +``` + +This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. + +#### .update( ) + +Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. + +```javascript +myDoughnutChart.segments[1].value = 10; +// Would update the first dataset's value of 'Green' to be 10 +myDoughnutChart.update(); +// Calling update now animates the circumference of the segment 'Green' from 50 to 10. +// and transitions other segment widths +``` + +#### .addData( segmentData, index ) + +Calling `addData(segmentData, index)` on your Chart instance passing an object in the same format as in the constructor. There is an option second argument of 'index', this determines at what index the new segment should be inserted into the chart. + +```javascript +// An object in the same format as the original data source +myDoughnutChart.addData({ + value: 130, + color: "#B48EAD", + highlight: "#C69CBE", + label: "Purple" +}); +// The new segment will now animate in. +``` + +#### .removeData( index ) + +Calling `removeData(index)` on your Chart instance will remove segment at that particular index. If none is provided, it will default to the last segment. + +```javascript +myDoughnutChart.removeData(); +// Other segments will update to fill the empty space left. +``` \ No newline at end of file diff --git a/docs/06-Advanced.md b/docs/06-Advanced.md new file mode 100644 index 00000000000..2330ef1608c --- /dev/null +++ b/docs/06-Advanced.md @@ -0,0 +1,144 @@ +--- +title: Advanced usage +anchor: advanced-usage +--- + + +### Prototype methods + +For each chart, there are a set of global prototype methods on the shared `ChartType` which you may find useful. These are available on all charts created with Chart.js, but for the examples, let's use a line chart we've made. + +```javascript +// For example: +var myLineChart = new Chart(ctx).Line(data); +``` + +#### .clear() + +Will clear the chart canvas. Used extensively internally between animation frames, but you might find it useful. + +```javascript +// Will clear the canvas that myLineChart is drawn on +myLineChart.clear(); +// => returns 'this' for chainability +``` + +#### .stop() + +Use this to stop any current animation loop. This will pause the chart during any current animation frame. Call `.render()` to re-animate. + +```javascript +// Stops the charts animation loop at its current frame +myLineChart.stop(); +// => returns 'this' for chainability +``` + +#### .resize() + +Use this to manually resize the canvas element. This is run each time the browser is resized, but you can call this method manually if you change the size of the canvas nodes container element. + +```javascript +// Resizes & redraws to fill its container element +myLineChart.resize(); +// => returns 'this' for chainability +``` + +#### .destroy() + +Use this to destroy any chart instances that are created. This will clean up any references stored to the chart object within Chart.js, along with any associated event listeners attached by Chart.js. + +```javascript +// Destroys a specific chart instance +myLineChart.destroy(); +``` + +#### .toBase64Image() + +This returns a base 64 encoded string of the chart in it's current state. + +```javascript +myLineChart.toBase64Image(); +// => returns png data url of the image on the canvas +``` + +#### .generateLegend() + +Returns an HTML string of a legend for that chart. The template for this legend is at `legendTemplate` in the chart options. + +```javascript +myLineChart.generateLegend(); +// => returns HTML string of a legend for this chart +``` + +### Writing new chart types + +Chart.js 1.0 has been rewritten to provide a platform for developers to create their own custom chart types, and be able to share and utilise them through the Chart.js API. + +The format is relatively simple, there are a set of utility helper methods under `Chart.helpers`, including things such as looping over collections, requesting animation frames, and easing equations. + +On top of this, there are also some simple base classes of Chart elements, these all extend from `Chart.Element`, and include things such as points, bars and scales. + +```javascript +Chart.Type.extend({ + // Passing in a name registers this chart in the Chart namespace + name: "Scatter", + // Providing a defaults will also register the deafults in the chart namespace + defaults : { + options: "Here", + available: "at this.options" + }, + // Initialize is fired when the chart is initialized - Data is passed in as a parameter + // Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + this.chart.ctx // The drawing context for this chart + this.chart.canvas // the canvas node for this chart + } +}); + +// Now we can create a new instance of our chart, using the Chart.js API +new Chart(ctx).Scatter(data); +// initialize is now run +``` + +### Extending existing chart types + +We can also extend existing chart types, and expose them to the API in the same way. Let's say for example, we might want to run some more code when we initialize every Line chart. + +```javascript +// Notice now we're extending the particular Line chart type, rather than the base class. +Chart.types.Line.extend({ + // Passing in a name registers this chart in the Chart namespace in the same way + name: "LineAlt", + initialize: function(data){ + console.log('My Line chart extension'); + Chart.types.Line.prototype.apply(this, arguments); + } +}); + +// Creates a line chart in the same way +new Chart(ctx).LineAlt(data); +// but this logs 'My Line chart extension' in the console. +``` + +### Creating custom builds + +Chart.js uses gulp to build the library into a single javascript file. We can use this same build script with custom parameters in order to build a custom version. + +Firstly, we need to ensure development dependencies are installed. With node and npm installed, after cloning the Chart.js repo to a local directory, and navigating to that directory in the command line, we can run the following: + +```bash +npm install +npm install -g gulp +``` + +This will install the local development dependencies for Chart.js, along with a CLI for the javascript task runner gulp. + +Now, we can run the `gulp build` task, and pass in a comma seperated list of types as an argument to build a custom version of Chart.js with only specified chart types. + +Here we will create a version of Chart.js with only Line, Radar and Bar charts included: + +```bash +gulp build --types=Line,Radar,Bar +``` + +This will output to the `/custom` directory, and write two files, Chart.js, and Chart.min.js with only those chart types included. diff --git a/docs/07-Notes.md b/docs/07-Notes.md new file mode 100644 index 00000000000..f4bb5afe2be --- /dev/null +++ b/docs/07-Notes.md @@ -0,0 +1,42 @@ +--- +title: Notes +anchor: notes +--- + +### Browser support +Browser support for the canvas element is available in all modern & major mobile browsers (caniuse.com/canvas). + +For IE8 & below, I would recommend using the polyfill ExplorerCanvas - available at https://code.google.com/p/explorercanvas/. It falls back to Internet explorer's format VML when canvas support is not available. Example use: + +```html + + + +``` + +Usually I would recommend feature detection to choose whether or not to load a polyfill, rather than IE conditional comments, however in this case, VML is a Microsoft proprietary format, so it will only work in IE. + +Some important points to note in my experience using ExplorerCanvas as a fallback. + +- Initialise charts on load rather than DOMContentReady when using the library, as sometimes a race condition will occur, and it will result in an error when trying to get the 2d context of a canvas. +- New VML DOM elements are being created for each animation frame and there is no hardware acceleration. As a result animation is usually slow and jerky, with flashing text. It is a good idea to dynamically turn off animation based on canvas support. I recommend using the excellent Modernizr to do this. +- When declaring fonts, the library explorercanvas requires the font name to be in single quotes inside the string. For example, instead of your scaleFontFamily property being simply "Arial", explorercanvas support, use "'Arial'" instead. Chart.js does this for default values. + +### Bugs & issues + +Please report these on the Github page - at github.com/nnnick/Chart.js. If you could include a link to a simple jsbin or similar to demonstrate the issue, that'd be really helpful. + + +### Contributing +New contributions to the library are welcome, just a couple of guidelines: + +- Tabs for indentation, not spaces please. +- Please ensure you're changing the individual files in `/src`, not the concatenated output in the `Chart.js` file in the root of the repo. +- Please check that your code will pass `jshint` code standards, `gulp jshint` will run this for you. +- Please keep pull requests concise, and document new functionality in the relevant `.md` file. +- Consider whether your changes are useful for all users, or if creating a Chart.js extension would be more appropriate. + +### License +Chart.js is open source and available under the MIT license. \ No newline at end of file From 94f22b656eea00500b6bcaca38293ebbfec276dd Mon Sep 17 00:00:00 2001 From: Nick Downie Date: Sun, 29 Jun 2014 18:36:04 +0100 Subject: [PATCH 5/9] Add in new chart samples --- samples/bar.html | 54 ++++++++++++++++++++----------------- samples/doughnut.html | 53 +++++++++++++++++++++++++----------- samples/line.html | 39 +++++++++++++++++---------- samples/pie.html | 52 ++++++++++++++++++++++++----------- samples/polar-area.html | 60 +++++++++++++++++++++++++++++++++++++++++ samples/radar.html | 56 ++++++++++++++++++++++---------------- 6 files changed, 221 insertions(+), 93 deletions(-) create mode 100644 samples/polar-area.html diff --git a/samples/bar.html b/samples/bar.html index bf150db8f57..5bf4b5bae36 100644 --- a/samples/bar.html +++ b/samples/bar.html @@ -3,37 +3,43 @@ Bar Chart - - - +
+ +
diff --git a/samples/doughnut.html b/samples/doughnut.html index 83f4c9e7c22..fdf7539a893 100644 --- a/samples/doughnut.html +++ b/samples/doughnut.html @@ -3,44 +3,65 @@ Doughnut Chart - - +
+ +
diff --git a/samples/line.html b/samples/line.html index 4c6734452ce..ccd0dad9653 100644 --- a/samples/line.html +++ b/samples/line.html @@ -3,41 +3,52 @@ Line Chart - - - +
+
+ +
+
diff --git a/samples/pie.html b/samples/pie.html index e3bbbaed3f7..255a4997618 100644 --- a/samples/pie.html +++ b/samples/pie.html @@ -1,38 +1,58 @@ - Radar Chart + Pie Chart - - - +
+ +
diff --git a/samples/polar-area.html b/samples/polar-area.html new file mode 100644 index 00000000000..d3d3f01b47e --- /dev/null +++ b/samples/polar-area.html @@ -0,0 +1,60 @@ + + + + Polar Area Chart + + + +
+ +
+ + + + + diff --git a/samples/radar.html b/samples/radar.html index 0ffdad91ce9..6a04f879c09 100644 --- a/samples/radar.html +++ b/samples/radar.html @@ -10,34 +10,44 @@ - +
+ +
From 10227f852b1f73ef0ceea05c7f4c4d39bda4ccd5 Mon Sep 17 00:00:00 2001 From: Nick Downie Date: Sun, 29 Jun 2014 18:36:25 +0100 Subject: [PATCH 6/9] Add src & built library files --- Chart.js | 4528 ++++++++++++++++++++++++++++------------ Chart.min.js | 49 +- src/Chart.Bar.js | 295 +++ src/Chart.Core.js | 1883 +++++++++++++++++ src/Chart.Doughnut.js | 184 ++ src/Chart.Line.js | 343 +++ src/Chart.PolarArea.js | 248 +++ src/Chart.Radar.js | 341 +++ 8 files changed, 6502 insertions(+), 1369 deletions(-) mode change 100755 => 100644 Chart.js create mode 100644 src/Chart.Bar.js create mode 100755 src/Chart.Core.js create mode 100644 src/Chart.Doughnut.js create mode 100644 src/Chart.Line.js create mode 100644 src/Chart.PolarArea.js create mode 100644 src/Chart.Radar.js diff --git a/Chart.js b/Chart.js old mode 100755 new mode 100644 index ffbe16f3711..3c813540999 --- a/Chart.js +++ b/Chart.js @@ -2,1425 +2,3293 @@ * Chart.js * http://chartjs.org/ * - * Copyright 2013 Nick Downie + * Copyright 2014 Nick Downie * Released under the MIT license * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md */ -//Define the global Chart Variable as a class. -window.Chart = function(context){ - var chart = this; - - - //Easing functions adapted from Robert Penner's easing equations - //http://www.robertpenner.com/easing/ - - var animationOptions = { - linear : function (t){ - return t; - }, - easeInQuad: function (t) { - return t*t; - }, - easeOutQuad: function (t) { - return -1 *t*(t-2); - }, - easeInOutQuad: function (t) { - if ((t/=1/2) < 1) return 1/2*t*t; - return -1/2 * ((--t)*(t-2) - 1); - }, - easeInCubic: function (t) { - return t*t*t; - }, - easeOutCubic: function (t) { - return 1*((t=t/1-1)*t*t + 1); - }, - easeInOutCubic: function (t) { - if ((t/=1/2) < 1) return 1/2*t*t*t; - return 1/2*((t-=2)*t*t + 2); - }, - easeInQuart: function (t) { - return t*t*t*t; - }, - easeOutQuart: function (t) { - 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; - return -1/2 * ((t-=2)*t*t*t - 2); - }, - easeInQuint: function (t) { - return 1*(t/=1)*t*t*t*t; - }, - easeOutQuint: function (t) { - 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; - return 1/2*((t-=2)*t*t*t*t + 2); - }, - easeInSine: function (t) { - return -1 * Math.cos(t/1 * (Math.PI/2)) + 1; - }, - easeOutSine: function (t) { - return 1 * Math.sin(t/1 * (Math.PI/2)); - }, - easeInOutSine: function (t) { - return -1/2 * (Math.cos(Math.PI*t/1) - 1); - }, - easeInExpo: function (t) { - return (t==0) ? 1 : 1 * Math.pow(2, 10 * (t/1 - 1)); - }, - easeOutExpo: function (t) { - 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)); - return 1/2 * (-Math.pow(2, -10 * --t) + 2); - }, - easeInCirc: function (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); - 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*.3; - if (a < Math.abs(1)) { a=1; var s=p/4; } - else var 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*.3; - if (a < Math.abs(1)) { a=1; var s=p/4; } - else var 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*(.3*1.5); - if (a < Math.abs(1)) { a=1; var s=p/4; } - else var s = p/(2*Math.PI) * Math.asin (1/a); - if (t < 1) return -.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 )*.5 + 1; - }, - easeInBack: function (t) { - var s = 1.70158; - return 1*(t/=1)*t*((s+1)*t - s); - }, - easeOutBack: function (t) { - var s = 1.70158; - return 1*((t=t/1-1)*t*((s+1)*t + s) + 1); - }, - easeInOutBack: function (t) { - var s = 1.70158; - 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) { - return 1 - animationOptions.easeOutBounce (1-t); - }, - easeOutBounce: function (t) { - if ((t/=1) < (1/2.75)) { - return 1*(7.5625*t*t); - } else if (t < (2/2.75)) { - return 1*(7.5625*(t-=(1.5/2.75))*t + .75); - } else if (t < (2.5/2.75)) { - return 1*(7.5625*(t-=(2.25/2.75))*t + .9375); - } else { - return 1*(7.5625*(t-=(2.625/2.75))*t + .984375); - } - }, - easeInOutBounce: function (t) { - if (t < 1/2) return animationOptions.easeInBounce (t*2) * .5; - return animationOptions.easeOutBounce (t*2-1) * .5 + 1*.5; - } - }; +(function(){ - //Variables global to the chart - var width = context.canvas.width; - var height = context.canvas.height; + "use strict"; + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart; - //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. - if (window.devicePixelRatio) { - context.canvas.style.width = width + "px"; - context.canvas.style.height = height + "px"; - context.canvas.height = height * window.devicePixelRatio; - context.canvas.width = width * window.devicePixelRatio; - context.scale(window.devicePixelRatio, window.devicePixelRatio); - } + //Occupy the global variable of Chart, and create a simple base class + var Chart = function(context){ + var chart = this; + this.canvas = context.canvas; - this.PolarArea = function(data,options){ - - chart.PolarArea.defaults = { - scaleOverlay : true, - scaleOverride : false, - scaleSteps : null, - scaleStepWidth : null, - scaleStartValue : null, - scaleShowLine : true, - scaleLineColor : "rgba(0,0,0,.1)", - scaleLineWidth : 1, - scaleShowLabels : true, - scaleLabel : "<%=value%>", - scaleFontFamily : "'Arial'", - scaleFontSize : 12, - scaleFontStyle : "normal", - scaleFontColor : "#666", - scaleShowLabelBackdrop : true, - scaleBackdropColor : "rgba(255,255,255,0.75)", - scaleBackdropPaddingY : 2, - scaleBackdropPaddingX : 2, - segmentShowStroke : true, - segmentStrokeColor : "#fff", - segmentStrokeWidth : 2, - animation : true, - animationSteps : 100, - animationEasing : "easeOutBounce", - animateRotate : true, - animateScale : false, - onAnimationComplete : null - }; - - var config = (options)? mergeChartConfig(chart.PolarArea.defaults,options) : chart.PolarArea.defaults; - - return new PolarArea(data,config,context); - }; + this.ctx = context; - this.Radar = function(data,options){ - - chart.Radar.defaults = { - scaleOverlay : false, - scaleOverride : false, - scaleSteps : null, - scaleStepWidth : null, - scaleStartValue : null, - scaleShowLine : true, - scaleLineColor : "rgba(0,0,0,.1)", - scaleLineWidth : 1, - scaleShowLabels : false, - scaleLabel : "<%=value%>", - scaleFontFamily : "'Arial'", - scaleFontSize : 12, - scaleFontStyle : "normal", - scaleFontColor : "#666", - scaleShowLabelBackdrop : true, - scaleBackdropColor : "rgba(255,255,255,0.75)", - scaleBackdropPaddingY : 2, - scaleBackdropPaddingX : 2, - angleShowLineOut : true, - angleLineColor : "rgba(0,0,0,.1)", - angleLineWidth : 1, - pointLabelFontFamily : "'Arial'", - pointLabelFontStyle : "normal", - pointLabelFontSize : 12, - pointLabelFontColor : "#666", - pointDot : true, - pointDotRadius : 3, - pointDotStrokeWidth : 1, - datasetStroke : true, - datasetStrokeWidth : 2, - datasetFill : true, - animation : true, - animationSteps : 60, - animationEasing : "easeOutQuart", - onAnimationComplete : null - }; - - var config = (options)? mergeChartConfig(chart.Radar.defaults,options) : chart.Radar.defaults; + //Variables global to the chart + var width = this.width = context.canvas.width; + var 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); - return new Radar(data,config,context); - }; - - this.Pie = function(data,options){ - chart.Pie.defaults = { - segmentShowStroke : true, - segmentStrokeColor : "#fff", - segmentStrokeWidth : 2, - animation : true, - animationSteps : 100, - animationEasing : "easeOutBounce", - animateRotate : true, - animateScale : false, - onAnimationComplete : null - }; - - var config = (options)? mergeChartConfig(chart.Pie.defaults,options) : chart.Pie.defaults; - - return new Pie(data,config,context); - }; - - this.Doughnut = function(data,options){ - - chart.Doughnut.defaults = { - segmentShowStroke : true, - segmentStrokeColor : "#fff", - segmentStrokeWidth : 2, - percentageInnerCutout : 50, - animation : true, - animationSteps : 100, - animationEasing : "easeOutBounce", - animateRotate : true, - animateScale : false, - onAnimationComplete : null - }; - - var config = (options)? mergeChartConfig(chart.Doughnut.defaults,options) : chart.Doughnut.defaults; - - return new Doughnut(data,config,context); - + return this; }; + //Globally expose the defaults to allow for user updating/changing + Chart.defaults = { + global: { + // Boolean - Whether to animate the chart + animation: true, - this.Line = function(data,options){ - - chart.Line.defaults = { - scaleOverlay : false, - scaleOverride : false, - scaleSteps : null, - scaleStepWidth : null, - scaleStartValue : null, - scaleLineColor : "rgba(0,0,0,.1)", - scaleLineWidth : 1, - scaleShowLabels : true, - scaleLabel : "<%=value%>", - scaleFontFamily : "'Arial'", - scaleFontSize : 12, - scaleFontStyle : "normal", - scaleFontColor : "#666", - scaleShowGridLines : true, - scaleGridLineColor : "rgba(0,0,0,.05)", - scaleGridLineWidth : 1, - bezierCurve : true, - pointDot : true, - pointDotRadius : 4, - pointDotStrokeWidth : 2, - datasetStroke : true, - datasetStrokeWidth : 2, - datasetFill : true, - animation : true, - animationSteps : 60, - animationEasing : "easeOutQuart", - onAnimationComplete : null - }; - var config = (options) ? mergeChartConfig(chart.Line.defaults,options) : chart.Line.defaults; - - return new Line(data,config,context); - } - - this.Bar = function(data,options){ - chart.Bar.defaults = { - scaleOverlay : false, - scaleOverride : false, - scaleSteps : null, - scaleStepWidth : null, - scaleStartValue : null, - scaleLineColor : "rgba(0,0,0,.1)", - scaleLineWidth : 1, - scaleShowLabels : true, - scaleLabel : "<%=value%>", - scaleFontFamily : "'Arial'", - scaleFontSize : 12, - scaleFontStyle : "normal", - scaleFontColor : "#666", - scaleShowGridLines : true, - scaleGridLineColor : "rgba(0,0,0,.05)", - scaleGridLineWidth : 1, - barShowStroke : true, - barStrokeWidth : 2, - barValueSpacing : 5, - barDatasetSpacing : 1, - animation : true, - animationSteps : 60, - animationEasing : "easeOutQuart", - onAnimationComplete : null - }; - var config = (options) ? mergeChartConfig(chart.Bar.defaults,options) : chart.Bar.defaults; - - return new Bar(data,config,context); - } - - var clear = function(c){ - c.clearRect(0, 0, width, height); - }; + // Number - Number of animation steps + animationSteps: 60, - var PolarArea = function(data,config,ctx){ - var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString; - - - calculateDrawingSizes(); - - valueBounds = getValueBounds(); + // String - Animation easing effect + animationEasing: "easeOutQuart", - labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : null; + // Boolean - If we should show the scale at all + showScale: true, - //Check and set the scale - if (!config.scaleOverride){ - - calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); - } - else { - calculatedScale = { - steps : config.scaleSteps, - stepValue : config.scaleStepWidth, - graphMin : config.scaleStartValue, - labels : [] - } - populateLabels(labelTemplateString, calculatedScale.labels,calculatedScale.steps,config.scaleStartValue,config.scaleStepWidth); - } - - scaleHop = maxSize/(calculatedScale.steps); - - //Wrap in an animation loop wrapper - animationLoop(config,drawScale,drawAllSegments,ctx); - - function calculateDrawingSizes(){ - maxSize = (Min([width,height])/2); - //Remove whatever is larger - the font size or line width. - - maxSize -= Max([config.scaleFontSize*0.5,config.scaleLineWidth*0.5]); - - labelHeight = config.scaleFontSize*2; - //If we're drawing the backdrop - add the Y padding to the label height and remove from drawing region. - if (config.scaleShowLabelBackdrop){ - labelHeight += (2 * config.scaleBackdropPaddingY); - maxSize -= config.scaleBackdropPaddingY*1.5; - } - - scaleHeight = maxSize; - //If the label height is less than 5, set it to 5 so we don't have lines on top of each other. - labelHeight = Default(labelHeight,5); - } - function drawScale(){ - for (var i=0; i upperValue) {upperValue = data[i].value;} - if (data[i].value < lowerValue) {lowerValue = data[i].value;} - }; + // Boolean - Whether to show labels on the scale + scaleShowLabels: true, - var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); - var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); - - return { - maxValue : upperValue, - minValue : lowerValue, - maxSteps : maxSteps, - minSteps : minSteps - }; - + // Interpolated JS string - can access value + scaleLabel: "<%=value%>", - } - } + // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there + scaleIntegersOnly: true, - var Radar = function (data,config,ctx) { - var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString; - - //If no labels are defined set to an empty array, so referencing length for looping doesn't blow up. - if (!data.labels) data.labels = []; - - calculateDrawingSizes(); + // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero: false, - var valueBounds = getValueBounds(); + // String - Scale label font declaration for the scale label + scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : null; + // Number - Scale label font size in pixels + scaleFontSize: 12, - //Check and set the scale - if (!config.scaleOverride){ - - calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); - } - else { - calculatedScale = { - steps : config.scaleSteps, - stepValue : config.scaleStepWidth, - graphMin : config.scaleStartValue, - labels : [] - } - populateLabels(labelTemplateString, calculatedScale.labels,calculatedScale.steps,config.scaleStartValue,config.scaleStepWidth); - } - - scaleHop = maxSize/(calculatedScale.steps); - - animationLoop(config,drawScale,drawAllDataPoints,ctx); - - //Radar specific functions. - function drawAllDataPoints(animationDecimal){ - var rotationDegree = (2*Math.PI)/data.datasets[0].data.length; - - ctx.save(); - //translate to the centre of the canvas. - ctx.translate(width/2,height/2); - - //We accept multiple data sets for radar charts, so show loop through each set - for (var i=0; i Math.PI){ - ctx.textAlign = "right"; - } - else{ - ctx.textAlign = "left"; - } - - ctx.textBaseline = "middle"; - - ctx.fillText(data.labels[k],opposite,-adjacent); - - } - ctx.restore(); - }; - function calculateDrawingSizes(){ - maxSize = (Min([width,height])/2); - - labelHeight = config.scaleFontSize*2; - - var labelLength = 0; - for (var i=0; ilabelLength) labelLength = textMeasurement; - } - - //Figure out whats the largest - the height of the text or the width of what's there, and minus it from the maximum usable size. - maxSize -= Max([labelLength,((config.pointLabelFontSize/2)*1.5)]); - - maxSize -= config.pointLabelFontSize; - maxSize = CapValue(maxSize, null, 0); - scaleHeight = maxSize; - //If the label height is less than 5, set it to 5 so we don't have lines on top of each other. - labelHeight = Default(labelHeight,5); - }; - function getValueBounds() { - var upperValue = Number.MIN_VALUE; - var lowerValue = Number.MAX_VALUE; - - for (var i=0; i upperValue){upperValue = data.datasets[i].data[j]} - if (data.datasets[i].data[j] < lowerValue){lowerValue = data.datasets[i].data[j]} - } - } + // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove + showTooltips: true, - var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); - var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); - - return { - maxValue : upperValue, - minValue : lowerValue, - maxSteps : maxSteps, - minSteps : minSteps - }; - + // Array - Array of string names to attach tooltip events + tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], - } - } + // String - Tooltip background colour + tooltipFillColor: "rgba(0,0,0,0.8)", - var Pie = function(data,config,ctx){ - var segmentTotal = 0; - - //In case we have a canvas that is not a square. Minus 5 pixels as padding round the edge. - var pieRadius = Min([height/2,width/2]) - 5; - - for (var i=0; i<%=label%>: <%}%><%= value %>", + + // String - Template string for single tooltips + multiTooltipTemplate: "<%= value %>", + + // String - Colour behind the legend colour block + multiTooltipKeyBackground: '#fff', + + // Function - Will fire on animation progression. + onAnimationProgress: function(){}, + + // Function - Will fire on animation completion. + onAnimationComplete: function(){} - var Line = function(data,config,ctx){ - var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString, valueHop,widestXLabel, xAxisLength,yAxisPosX,xAxisPosY, rotateLabels = 0; - - calculateDrawingSizes(); - - valueBounds = getValueBounds(); - //Check and set the scale - labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : ""; - if (!config.scaleOverride){ - - calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); - } - else { - calculatedScale = { - steps : config.scaleSteps, - stepValue : config.scaleStepWidth, - graphMin : config.scaleStartValue, - labels : [] - } - populateLabels(labelTemplateString, calculatedScale.labels,calculatedScale.steps,config.scaleStartValue,config.scaleStepWidth); } - - scaleHop = Math.floor(scaleHeight/calculatedScale.steps); - calculateXAxisSize(); - animationLoop(config,drawScale,drawLines,ctx); - - function drawLines(animPc){ - for (var i=0; i 0){ - ctx.save(); - ctx.textAlign = "right"; + }, + clone = helpers.clone = function(obj){ + var objClone = {}; + each(obj,function(value,key){ + 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; + }); + }); + return base; + }, + merge = helpers.merge = function(base,master){ + //Merge properties in left object over to a shallow clone of object right. + var args = Array.prototype.slice.call(arguments,0); + args.unshift({}); + return extend.apply(null, args); + }, + indexOf = helpers.indexOf = function(arrayToSearch, item){ + if (Array.prototype.indexOf) { + return arrayToSearch.indexOf(item); } else{ - ctx.textAlign = "center"; - } - ctx.fillStyle = config.scaleFontColor; - for (var i=0; i 0){ - ctx.translate(yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize); - ctx.rotate(-(rotateLabels * (Math.PI/180))); - ctx.fillText(data.labels[i], 0,0); - ctx.restore(); - } - - else{ - ctx.fillText(data.labels[i], yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize+3); - } - - ctx.beginPath(); - ctx.moveTo(yAxisPosX + i * valueHop, xAxisPosY+3); - - //Check i isnt 0, so we dont go over the Y axis twice. - if(config.scaleShowGridLines && i>0){ - ctx.lineWidth = config.scaleGridLineWidth; - ctx.strokeStyle = config.scaleGridLineColor; - ctx.lineTo(yAxisPosX + i * valueHop, 5); - } - else{ - ctx.lineTo(yAxisPosX + i * valueHop, xAxisPosY+3); + for (var i = 0; i < arrayToSearch.length; i++) { + if (arrayToSearch[i] === item) return i; } - ctx.stroke(); + return -1; } - - //Y axis - ctx.lineWidth = config.scaleLineWidth; - ctx.strokeStyle = config.scaleLineColor; - ctx.beginPath(); - ctx.moveTo(yAxisPosX,xAxisPosY+5); - ctx.lineTo(yAxisPosX,5); - ctx.stroke(); - - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - for (var j=0; j maxValue ) { + return maxValue; } } - - - } - function calculateXAxisSize(){ - var longestText = 1; - //if we are showing the labels - if (config.scaleShowLabels){ - ctx.font = config.scaleFontStyle + " " + config.scaleFontSize+"px " + config.scaleFontFamily; - for (var i=0; i longestText)? measuredText : longestText; + else if(isNumber(minValue)){ + if ( valueToCap < minValue ){ + return minValue; } - //Add a little extra padding from the y axis - longestText +=10; } - xAxisLength = width - longestText - widestXLabel; - valueHop = Math.floor(xAxisLength/(data.labels.length-1)); - - yAxisPosX = width-widestXLabel/2-xAxisLength; - xAxisPosY = scaleHeight + config.scaleFontSize/2; - } - function calculateDrawingSizes(){ - maxSize = height; - - //Need to check the X axis first - measure the length of each text metric, and figure out if we need to rotate by 45 degrees. - ctx.font = config.scaleFontStyle + " " + config.scaleFontSize+"px " + config.scaleFontFamily; - widestXLabel = 1; - for (var i=0; i widestXLabel)? textLength : widestXLabel; + return valueToCap; + }, + getDecimalPlaces = helpers.getDecimalPlaces = function(num){ + if (num%1!==0 && isNumber(num)){ + return num.toString().split(".")[1].length; } - if (width/data.labels.length < widestXLabel){ - rotateLabels = 45; - if (width/data.labels.length < Math.cos(rotateLabels) * widestXLabel){ - rotateLabels = 90; - maxSize -= widestXLabel; - } - else{ - maxSize -= Math.sin(rotateLabels) * widestXLabel; - } + else { + return 0; } - else{ - maxSize -= config.scaleFontSize; + }, + toRadians = helpers.radians = function(degrees){ + return degrees * (Math.PI/180); + }, + // Gets the angle from vertical upright to the point about a centre. + getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ + var distanceFromXCenter = anglePoint.x - centrePoint.x, + distanceFromYCenter = anglePoint.y - centrePoint.y, + radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + + + var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); + + //If the segment is in the top left quadrant, we need to add another rotation to the angle + if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ + angle += Math.PI*2; } - - //Add a little padding between the x line and the text - maxSize -= 5; - - - labelHeight = config.scaleFontSize; - - maxSize -= labelHeight; - //Set 5 pixels greater than the font size to allow for a little padding from the X axis. - - scaleHeight = maxSize; - - //Then get the area above we can safely draw on. - - } - function getValueBounds() { - var upperValue = Number.MIN_VALUE; - var lowerValue = Number.MAX_VALUE; - for (var i=0; i upperValue) { upperValue = data.datasets[i].data[j] }; - if ( data.datasets[i].data[j] < lowerValue) { lowerValue = data.datasets[i].data[j] }; - } + + return { + angle: angle, + distance: radialDistanceFromCenter }; - - var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); - var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); - + }, + aliasPixel = helpers.aliasPixel = function(pixelWidth){ + return (pixelWidth % 2 === 0) ? 0 : 0.5; + }, + splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ + //Props to Rob Spencer at scaled innovation for his post on splining between points + //http://scaledinnovation.com/analytics/splines/aboutSplines.html + var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), + d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), + fa=t*d01/(d01+d12),// scaling factor for triangle Ta + fb=t*d12/(d01+d12); return { - maxValue : upperValue, - minValue : lowerValue, - maxSteps : maxSteps, - minSteps : minSteps + inner : { + x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) + }, + outer : { + x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) + } }; - - - } + }, + calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ + return Math.floor(Math.log(val) / Math.LN10); + }, + calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ - - } - - var Bar = function(data,config,ctx){ - var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString, valueHop,widestXLabel, xAxisLength,yAxisPosX,xAxisPosY,barWidth, rotateLabels = 0; - - calculateDrawingSizes(); - - valueBounds = getValueBounds(); - //Check and set the scale - labelTemplateString = (config.scaleShowLabels)? config.scaleLabel : ""; - if (!config.scaleOverride){ - - calculatedScale = calculateScale(scaleHeight,valueBounds.maxSteps,valueBounds.minSteps,valueBounds.maxValue,valueBounds.minValue,labelTemplateString); - } - else { - calculatedScale = { - steps : config.scaleSteps, - stepValue : config.scaleStepWidth, - graphMin : config.scaleStartValue, - labels : [] - } - populateLabels(labelTemplateString, calculatedScale.labels,calculatedScale.steps,config.scaleStartValue,config.scaleStepWidth); - } - - scaleHop = Math.floor(scaleHeight/calculatedScale.steps); - calculateXAxisSize(); - animationLoop(config,drawScale,drawBars,ctx); - - function drawBars(animPc){ - ctx.lineWidth = config.barStrokeWidth; - for (var i=0; i= maxSteps); + + 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 + // Adding/minusing 0.5 will give us a range of 1. + if (maxValue === minValue){ + maxValue += 0.5; + // So we don't end up with a graph with a negative start value if we've said always start from zero + if (minValue >= 0.5 && !startFromZero){ + minValue -= 0.5; + } + else{ + // Make up a whole number above the values + maxValue += 0.5; } } - - } - function drawScale(){ - //X axis line - ctx.lineWidth = config.scaleLineWidth; - ctx.strokeStyle = config.scaleLineColor; - ctx.beginPath(); - ctx.moveTo(width-widestXLabel/2+5,xAxisPosY); - ctx.lineTo(width-(widestXLabel/2)-xAxisLength-5,xAxisPosY); - ctx.stroke(); - - - if (rotateLabels > 0){ - ctx.save(); - ctx.textAlign = "right"; - } - else{ - ctx.textAlign = "center"; - } - ctx.fillStyle = config.scaleFontColor; - for (var i=0; i 0){ - ctx.translate(yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize); - ctx.rotate(-(rotateLabels * (Math.PI/180))); - ctx.fillText(data.labels[i], 0,0); - ctx.restore(); + + var valueRange = Math.abs(maxValue - minValue), + rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), + graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphRange = graphMax - graphMin, + stepValue = Math.pow(10, rangeOrderOfMagnitude), + numberOfSteps = Math.round(graphRange / stepValue); + + //If we have more space on the graph we'll use it to give more definition to the data + while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { + if(numberOfSteps > maxSteps){ + stepValue *=2; + numberOfSteps = Math.round(graphRange/stepValue); + // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. + if (numberOfSteps % 1 !== 0){ + skipFitting = true; + } } - + //We can fit in double the amount of scale points on the scale else{ - ctx.fillText(data.labels[i], yAxisPosX + i*valueHop + valueHop/2,xAxisPosY + config.scaleFontSize+3); + //If user has declared ints only, and the step value isn't a decimal + if (integersOnly && rangeOrderOfMagnitude >= 0){ + //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float + if(stepValue/2 % 1 === 0){ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + //If it would make it a float break out of the loop + else{ + break; + } + } + //If the scale doesn't have to be an int, make the scale more granular anyway. + else{ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + } + } - ctx.beginPath(); - ctx.moveTo(yAxisPosX + (i+1) * valueHop, xAxisPosY+3); - - //Check i isnt 0, so we dont go over the Y axis twice. - ctx.lineWidth = config.scaleGridLineWidth; - ctx.strokeStyle = config.scaleGridLineColor; - ctx.lineTo(yAxisPosX + (i+1) * valueHop, 5); - ctx.stroke(); + if (skipFitting){ + numberOfSteps = minSteps; + stepValue = graphRange / numberOfSteps; } - - //Y axis - ctx.lineWidth = config.scaleLineWidth; - ctx.strokeStyle = config.scaleLineColor; - ctx.beginPath(); - ctx.moveTo(yAxisPosX,xAxisPosY+5); - ctx.lineTo(yAxisPosX,5); - ctx.stroke(); - - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - for (var j=0; j)[^\t]*)'/g, "$1\r") + .replace(/\t=(.*?)%>/g, "',$1,'") + .split("\t").join("');") + .split("%>").join("p.push('") + .split("\r").join("\\'") + + "');}return p.join('');" + ); + + // Provide some basic currying to the user + return data ? fn( data ) : fn; + } + return tmpl(templateString,valuesObject); + }, + /* jshint ignore:end */ + generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ + var labelsArray = new Array(numberOfSteps); + if (labelTemplateString){ + each(labelsArray,function(val,index){ + labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); + }); + } + return labelsArray; + }, + //--Animation methods + //Easing functions adapted from Robert Penner's easing equations + //http://www.robertpenner.com/easing/ + easingEffects = helpers.easingEffects = { + linear: function (t) { + return t; + }, + easeInQuad: function (t) { + return t * t; + }, + easeOutQuad: function (t) { + return -1 * t * (t - 2); + }, + easeInOutQuad: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; + return -1 / 2 * ((--t) * (t - 2) - 1); + }, + easeInCubic: function (t) { + return t * t * t; + }, + easeOutCubic: function (t) { + return 1 * ((t = t / 1 - 1) * t * t + 1); + }, + easeInOutCubic: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; + return 1 / 2 * ((t -= 2) * t * t + 2); + }, + easeInQuart: function (t) { + return t * t * t * t; + }, + easeOutQuart: function (t) { + 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; + return -1 / 2 * ((t -= 2) * t * t * t - 2); + }, + easeInQuint: function (t) { + return 1 * (t /= 1) * t * t * t * t; + }, + easeOutQuint: function (t) { + 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; + return 1 / 2 * ((t -= 2) * t * t * t * t + 2); + }, + easeInSine: function (t) { + return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; + }, + easeOutSine: function (t) { + return 1 * Math.sin(t / 1 * (Math.PI / 2)); + }, + easeInOutSine: function (t) { + return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); + }, + easeInExpo: function (t) { + return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); + }, + easeOutExpo: function (t) { + 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)); + return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); + }, + easeInCirc: function (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); + 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 (a < Math.abs(1)) { + a = 1; + s = p / 4; + } 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 (a < Math.abs(1)) { + a = 1; + s = p / 4; + } 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 (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)); + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; + }, + easeInBack: function (t) { + var s = 1.70158; + return 1 * (t /= 1) * t * ((s + 1) * t - s); + }, + easeOutBack: function (t) { + var s = 1.70158; + return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); + }, + easeInOutBack: function (t) { + var s = 1.70158; + 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) { + return 1 - easingEffects.easeOutBounce(1 - t); + }, + easeOutBounce: function (t) { + if ((t /= 1) < (1 / 2.75)) { + return 1 * (7.5625 * t * t); + } else if (t < (2 / 2.75)) { + return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); + } else if (t < (2.5 / 2.75)) { + return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); + } else { + return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); } - - ctx.stroke(); - if (config.scaleShowLabels){ - ctx.fillText(calculatedScale.labels[j],yAxisPosX-8,xAxisPosY - ((j+1) * scaleHop)); + }, + easeInOutBounce: function (t) { + if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; + return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; + } + }, + //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ + requestAnimFrame = helpers.requestAnimFrame = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, 1000 / 60); + }; + })(), + cancelAnimFrame = helpers.cancelAnimFrame = (function(){ + return window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.mozCancelAnimationFrame || + window.oCancelAnimationFrame || + window.msCancelAnimationFrame || + function(callback) { + return window.clearTimeout(callback, 1000 / 60); + }; + })(), + animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ + + var currentStep = 0, + easingFunction = easingEffects[easingString] || easingEffects.linear; + + var animationFrame = function(){ + currentStep++; + var stepDecimal = currentStep/totalSteps; + var easeDecimal = easingFunction(stepDecimal); + + callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); + onProgress.call(chartInstance,easeDecimal,stepDecimal); + if (currentStep < totalSteps){ + chartInstance.animationFrame = requestAnimFrame(animationFrame); + } else{ + onComplete.apply(chartInstance); } + }; + requestAnimFrame(animationFrame); + }, + //-- DOM methods + getRelativePosition = helpers.getRelativePosition = function(evt){ + var mouseX, mouseY; + var e = evt.originalEvent || evt, + canvas = evt.currentTarget || evt.srcElement, + boundingRect = canvas.getBoundingClientRect(); + + if (e.touches){ + mouseX = e.touches[0].clientX - boundingRect.left; + mouseY = e.touches[0].clientY - boundingRect.top; + + } + else{ + mouseX = e.clientX - boundingRect.left; + mouseY = e.clientY - boundingRect.top; + } + + return { + x : mouseX, + y : mouseY + }; + + }, + addEvent = helpers.addEvent = function(node,eventType,method){ + if (node.addEventListener){ + node.addEventListener(eventType,method); + } else if (node.attachEvent){ + node.attachEvent("on"+eventType, method); + } else { + node["on"+eventType] = method; + } + }, + removeEvent = helpers.removeEvent = function(node, eventType, handler){ + if (node.removeEventListener){ + node.removeEventListener(eventType, handler, false); + } else if (node.detachEvent){ + node.detachEvent("on"+eventType,handler); + } else{ + node["on" + eventType] = noop; + } + }, + bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ + // Create the events object if it's not already present + if (!chartInstance.events) chartInstance.events = {}; + + each(arrayOfEvents,function(eventName){ + chartInstance.events[eventName] = function(){ + handler.apply(chartInstance, arguments); + }; + addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); + }); + }, + unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { + each(arrayOfEvents, function(handler,eventName){ + removeEvent(chartInstance.chart.canvas, eventName, handler); + }); + }, + getMaximumSize = helpers.getMaximumSize = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientWidth; + }, + retinaScale = helpers.retinaScale = function(chart){ + var ctx = chart.ctx, + width = chart.canvas.width, + height = chart.canvas.height; + //console.log(width + " x " + height); + if (window.devicePixelRatio) { + ctx.canvas.style.width = width + "px"; + ctx.canvas.style.height = height + "px"; + ctx.canvas.height = height * window.devicePixelRatio; + ctx.canvas.width = width * window.devicePixelRatio; + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); } - - + }, + //-- Canvas methods + clear = helpers.clear = function(chart){ + chart.ctx.clearRect(0,0,chart.width,chart.height); + }, + fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ + return fontStyle + " " + pixelSize+"px " + fontFamily; + }, + longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ + ctx.font = font; + var longest = 0; + each(arrayOfStrings,function(string){ + var textWidth = ctx.measureText(string).width; + longest = (textWidth > longest) ? textWidth : longest; + }); + return longest; + }, + drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + }; + + + //Store a reference to each instance - allowing us to globally resize chart instances on window resize. + //Destroy method on the chart will remove the instance of the chart from this reference. + Chart.instances = {}; + + Chart.Type = function(data,options,chart){ + this.options = options; + this.chart = chart; + this.id = uid(); + //Add the chart instance to the global namespace + Chart.instances[this.id] = this; + + // Initialize is always called when a chart type is created + // By default it is a no op, but it should be extended + if (options.responsive){ + this.resize(); } - function calculateXAxisSize(){ - var longestText = 1; - //if we are showing the labels - if (config.scaleShowLabels){ - ctx.font = config.scaleFontStyle + " " + config.scaleFontSize+"px " + config.scaleFontFamily; - for (var i=0; i longestText)? measuredText : longestText; - } - //Add a little extra padding from the y axis - longestText +=10; + this.initialize.call(this,data); + }; + + //Core methods that'll be a part of every chart type + extend(Chart.Type.prototype,{ + initialize : function(){return this;}, + clear : function(){ + clear(this.chart); + return this; + }, + stop : function(){ + // Stops any current animation loop occuring + helpers.cancelAnimFrame.call(root, this.animationFrame); + return this; + }, + resize : function(callback){ + this.stop(); + var canvas = this.chart.canvas, + newWidth = getMaximumSize(this.chart.canvas), + newHeight = newWidth / this.chart.aspectRatio; + + canvas.width = this.chart.width = newWidth; + canvas.height = this.chart.height = newHeight; + + retinaScale(this.chart); + + if (typeof callback === "function"){ + callback.apply(this, Array.prototype.slice.call(arguments, 1)); } - xAxisLength = width - longestText - widestXLabel; - valueHop = Math.floor(xAxisLength/(data.labels.length)); - - barWidth = (valueHop - config.scaleGridLineWidth*2 - (config.barValueSpacing*2) - (config.barDatasetSpacing*data.datasets.length-1) - ((config.barStrokeWidth/2)*data.datasets.length-1))/data.datasets.length; - - yAxisPosX = width-widestXLabel/2-xAxisLength; - xAxisPosY = scaleHeight + config.scaleFontSize/2; - } - function calculateDrawingSizes(){ - maxSize = height; - - //Need to check the X axis first - measure the length of each text metric, and figure out if we need to rotate by 45 degrees. - ctx.font = config.scaleFontStyle + " " + config.scaleFontSize+"px " + config.scaleFontFamily; - widestXLabel = 1; - for (var i=0; i widestXLabel)? textLength : widestXLabel; + return this; + }, + reflow : noop, + render : function(reflow){ + if (reflow){ + this.reflow(); } - if (width/data.labels.length < widestXLabel){ - rotateLabels = 45; - if (width/data.labels.length < Math.cos(rotateLabels) * widestXLabel){ - rotateLabels = 90; - maxSize -= widestXLabel; - } - else{ - maxSize -= Math.sin(rotateLabels) * widestXLabel; + if (this.options.animation && !reflow){ + helpers.animationLoop( + this.draw, + this.options.animationSteps, + this.options.animationEasing, + this.options.onAnimationProgress, + this.options.onAnimationComplete, + this + ); + } + else{ + this.draw(); + this.options.onAnimationComplete.call(this); + } + return this; + }, + generateLegend : function(){ + return template(this.options.legendTemplate,this); + }, + destroy : function(){ + this.clear(); + unbindEvents(this, this.events); + delete Chart.instances[this.id]; + }, + showTooltip : function(ChartElements, forceRedraw){ + // Only redraw the chart if we've actually changed what we're hovering on. + if (typeof this.activeElements === 'undefined') this.activeElements = []; + + var isChanged = (function(Elements){ + var changed = false; + + if (Elements.length !== this.activeElements.length){ + changed = true; + return changed; } + + each(Elements, function(element, index){ + if (element !== this.activeElements[index]){ + changed = true; + } + }, this); + return changed; + }).call(this, ChartElements); + + if (!isChanged && !forceRedraw){ + return; } else{ - maxSize -= config.scaleFontSize; + this.activeElements = ChartElements; } - - //Add a little padding between the x line and the text - maxSize -= 5; - - - labelHeight = config.scaleFontSize; - - maxSize -= labelHeight; - //Set 5 pixels greater than the font size to allow for a little padding from the X axis. - - scaleHeight = maxSize; - - //Then get the area above we can safely draw on. - - } - function getValueBounds() { - var upperValue = Number.MIN_VALUE; - var lowerValue = Number.MAX_VALUE; - for (var i=0; i upperValue) { upperValue = data.datasets[i].data[j] }; - if ( data.datasets[i].data[j] < lowerValue) { lowerValue = data.datasets[i].data[j] }; + this.draw(); + if (ChartElements.length > 0){ + // If we have multiple datasets, show a MultiTooltip for all of the data points at that index + if (this.datasets && this.datasets.length > 1) { + var dataArray, + dataIndex; + + for (var i = this.datasets.length - 1; i >= 0; i--) { + dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; + dataIndex = indexOf(dataArray, ChartElements[0]); + if (dataIndex !== -1){ + break; + } + } + var tooltipLabels = [], + tooltipColors = [], + medianPosition = (function(index) { + + // Get all the points at that particular index + var Elements = [], + dataCollection, + xPositions = [], + yPositions = [], + xMax, + yMax, + xMin, + yMin; + helpers.each(this.datasets, function(dataset){ + dataCollection = dataset.points || dataset.bars || dataset.segments; + Elements.push(dataCollection[dataIndex]); + }); + + helpers.each(Elements, function(element) { + xPositions.push(element.x); + yPositions.push(element.y); + + + //Include any colour information about the element + tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); + tooltipColors.push({ + fill: element._saved.fillColor || element.fillColor, + stroke: element._saved.strokeColor || element.strokeColor + }); + + }, this); + + yMin = min(yPositions); + yMax = max(yPositions); + + xMin = min(xPositions); + xMax = max(xPositions); + + return { + x: (xMin > this.chart.width/2) ? xMin : xMax, + y: (yMin + yMax)/2 + }; + }).call(this, dataIndex); + + new Chart.MultiTooltip({ + x: medianPosition.x, + y: medianPosition.y, + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + xOffset: this.options.tooltipXOffset, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + titleTextColor: this.options.tooltipTitleFontColor, + titleFontFamily: this.options.tooltipTitleFontFamily, + titleFontStyle: this.options.tooltipTitleFontStyle, + titleFontSize: this.options.tooltipTitleFontSize, + cornerRadius: this.options.tooltipCornerRadius, + labels: tooltipLabels, + legendColors: tooltipColors, + legendColorBackground : this.options.multiTooltipKeyBackground, + title: ChartElements[0].label, + chart: this.chart, + ctx: this.chart.ctx + }).draw(); + + } else { + each(ChartElements, function(Element) { + var tooltipPosition = Element.tooltipPosition(); + new Chart.Tooltip({ + x: Math.round(tooltipPosition.x), + y: Math.round(tooltipPosition.y), + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + caretHeight: this.options.tooltipCaretSize, + cornerRadius: this.options.tooltipCornerRadius, + text: template(this.options.tooltipTemplate, Element), + chart: this.chart + }).draw(); + }, this); } + } + return this; + }, + toBase64Image : function(){ + return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); + } + }); + + Chart.Type.extend = function(extensions){ + + var parent = this; + + var ChartType = function(){ + return parent.apply(this,arguments); + }; + + //Copy the prototype object of the this class + ChartType.prototype = clone(parent.prototype); + //Now overwrite some of the properties in the base class with the new extensions + extend(ChartType.prototype, extensions); + + ChartType.extend = Chart.Type.extend; + + if (extensions.name || parent.prototype.name){ + + var chartName = extensions.name || parent.prototype.name; + //Assign any potential default values of the new chart type + + //If none are defined, we'll use a clone of the chart type this is being extended from. + //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart + //doesn't define some defaults of their own. + + var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; + + Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); + + Chart.types[chartName] = ChartType; + + //Register this new chart type in the Chart prototype + Chart.prototype[chartName] = function(data,options){ + var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); + return new ChartType(data,config,this); }; - - var maxSteps = Math.floor((scaleHeight / (labelHeight*0.66))); - var minSteps = Math.floor((scaleHeight / labelHeight*0.5)); - + } else{ + warn("Name not provided for this chart, so it hasn't been registered"); + } + return parent; + }; + + Chart.Element = function(configuration){ + extend(this,configuration); + this.initialize.apply(this,arguments); + this.save(); + }; + extend(Chart.Element.prototype,{ + initialize : function(){}, + restore : function(props){ + if (!props){ + extend(this,this._saved); + } else { + each(props,function(key){ + this[key] = this._saved[key]; + },this); + } + return this; + }, + save : function(){ + this._saved = clone(this); + delete this._saved._saved; + return this; + }, + update : function(newProps){ + each(newProps,function(value,key){ + this._saved[key] = this[key]; + this[key] = value; + },this); + return this; + }, + transition : function(props,ease){ + each(props,function(value,key){ + this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; + },this); + return this; + }, + tooltipPosition : function(){ return { - maxValue : upperValue, - minValue : lowerValue, - maxSteps : maxSteps, - minSteps : minSteps + x : this.x, + y : this.y }; - - } - } - - function calculateOffset(val,calculatedScale,scaleHop){ - var outerValue = calculatedScale.steps * calculatedScale.stepValue; - var adjustedValue = val - calculatedScale.graphMin; - var scalingFactor = CapValue(adjustedValue/outerValue,1,0); - return (scaleHop*calculatedScale.steps) * scalingFactor; - } - - function animationLoop(config,drawScale,drawData,ctx){ - var animFrameAmount = (config.animation)? 1/CapValue(config.animationSteps,Number.MAX_VALUE,1) : 1, - easingFunction = animationOptions[config.animationEasing], - percentAnimComplete =(config.animation)? 0 : 1; - - - - if (typeof drawScale !== "function") drawScale = function(){}; - - requestAnimFrame(animLoop); - - function animateFrame(){ - var easeAdjustedAnimationPercent =(config.animation)? CapValue(easingFunction(percentAnimComplete),null,0) : 1; - clear(ctx); - if(config.scaleOverlay){ - drawData(easeAdjustedAnimationPercent); - drawScale(); - } else { - drawScale(); - drawData(easeAdjustedAnimationPercent); - } + }); + + Chart.Element.extend = inherits; + + + Chart.Point = Chart.Element.extend({ + inRange : function(chartX,chartY){ + var hitDetectionRange = this.hitDetectionRadius + this.radius; + return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); + }, + draw : function(){ + var ctx = this.ctx; + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); + ctx.closePath(); + + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.stroke(); + + + + //Quick debug for bezier curve splining + //Highlights control points and the line between them. + //Handy for dev - stripped in the min version. + + // ctx.save(); + // ctx.fillStyle = "black"; + // ctx.strokeStyle = "black" + // ctx.beginPath(); + // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.beginPath(); + // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); + // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); + // ctx.stroke(); + + // ctx.restore(); + + + } - function animLoop(){ - //We need to check if the animation is incomplete (less than 1), or complete (1). - percentAnimComplete += animFrameAmount; - animateFrame(); - //Stop the loop continuing forever - if (percentAnimComplete <= 1){ - requestAnimFrame(animLoop); - } - else{ - if (typeof config.onAnimationComplete == "function") config.onAnimationComplete(); - } - - } - - } + }); - //Declare global functions to be called within this namespace here. - - - // shim layer with setTimeout fallback - var requestAnimFrame = (function(){ - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback) { - window.setTimeout(callback, 1000 / 60); + Chart.Arc = Chart.Element.extend({ + inRange : function(chartX,chartY){ + + var pointRelativePosition = helpers.getAngleFromPoint(this, { + x: chartX, + y: chartY + }); + + //Check if within the range of the open/close angle + var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), + withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); + + return (betweenAngles && withinRadius); + //Ensure within the outside of the arc centre, but inside arc outer + }, + tooltipPosition : function(){ + var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), + rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; + return { + x : this.x + (Math.cos(centreAngle) * rangeFromCentre), + y : this.y + (Math.sin(centreAngle) * rangeFromCentre) }; - })(); - - function calculateScale(drawingHeight,maxSteps,minSteps,maxValue,minValue,labelTemplateString){ - var graphMin,graphMax,graphRange,stepValue,numberOfSteps,valueRange,rangeOrderOfMagnitude,decimalNum; - - valueRange = maxValue - minValue; - - rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange); - - graphMin = Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude); - - graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude); - - graphRange = graphMax - graphMin; - - stepValue = Math.pow(10, rangeOrderOfMagnitude); - - numberOfSteps = Math.round(graphRange / stepValue); - - //Compare number of steps to the max and min for that size graph, and add in half steps if need be. - while(numberOfSteps < minSteps || numberOfSteps > maxSteps) { - if (numberOfSteps < minSteps){ - stepValue /= 2; - numberOfSteps = Math.round(graphRange/stepValue); - } - else{ - stepValue *=2; - numberOfSteps = Math.round(graphRange/stepValue); - } - }; - - var labels = []; - populateLabels(labelTemplateString, labels, numberOfSteps, graphMin, stepValue); - - return { - steps : numberOfSteps, - stepValue : stepValue, - graphMin : graphMin, - labels : labels - - } - - function calculateOrderOfMagnitude(val){ - return Math.floor(Math.log(val) / Math.LN10); - } + }, + draw : function(animationPercent){ + var easingDecimal = animationPercent || 1; - } + var ctx = this.ctx; - //Populate an array of all the labels by interpolating the string. - function populateLabels(labelTemplateString, labels, numberOfSteps, graphMin, stepValue) { - if (labelTemplateString) { - //Fix floating point errors by setting to fixed the on the same decimal as the stepValue. - for (var i = 1; i < numberOfSteps + 1; i++) { - labels.push(tmpl(labelTemplateString, {value: (graphMin + (stepValue * i)).toFixed(getDecimalPlaces(stepValue))})); - } - } - } - - //Max value from array - function Max( array ){ - return Math.max.apply( Math, array ); - }; - //Min value from array - function Min( array ){ - return Math.min.apply( Math, array ); - }; - //Default if undefined - function Default(userDeclared,valueIfFalse){ - if(!userDeclared){ - return valueIfFalse; - } else { - return userDeclared; - } - }; - //Is a number function - function isNumber(n) { - return !isNaN(parseFloat(n)) && isFinite(n); - } - //Apply cap a value at a high or low number - function CapValue(valueToCap, maxValue, minValue){ - if(isNumber(maxValue)) { - if( valueToCap > maxValue ) { - return maxValue; + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); + + ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); + + ctx.closePath(); + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.lineJoin = 'bevel'; + + if (this.showStroke){ + ctx.stroke(); } } - if(isNumber(minValue)){ - if ( valueToCap < minValue ){ - return minValue; + }); + + Chart.Rectangle = Chart.Element.extend({ + draw : function(){ + var ctx = this.ctx, + halfWidth = this.width/2, + leftX = this.x - halfWidth, + rightX = this.x + halfWidth, + top = this.base - (this.base - this.y), + halfStroke = this.strokeWidth / 2; + + // Canvas doesn't allow us to stroke inside the width so we can + // adjust the sizes to fit if we're setting a stroke on the line + if (this.showStroke){ + leftX += halfStroke; + rightX -= halfStroke; + top += halfStroke; } + + ctx.beginPath(); + + ctx.fillStyle = this.fillColor; + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + // It'd be nice to keep this class totally generic to any rectangle + // and simply specify which border to miss out. + ctx.moveTo(leftX, this.base); + ctx.lineTo(leftX, top); + ctx.lineTo(rightX, top); + ctx.lineTo(rightX, this.base); + ctx.fill(); + if (this.showStroke){ + ctx.stroke(); + } + }, + height : function(){ + return this.base - this.y; + }, + inRange : function(chartX,chartY){ + return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); } - return valueToCap; - } - function getDecimalPlaces (num){ - var numberOfDecimalPlaces; - if (num%1!=0){ - return num.toString().split(".")[1].length - } - else{ - return 0; + }); + + Chart.Tooltip = Chart.Element.extend({ + draw : function(){ + + var ctx = this.chart.ctx; + + ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.xAlign = "center"; + this.yAlign = "above"; + + //Distance between the actual element.y position and the start of the tooltip caret + var caretPadding = 2; + + var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, + tooltipRectHeight = this.fontSize + 2*this.yPadding, + tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; + + if (this.x + tooltipWidth/2 >this.chart.width){ + this.xAlign = "left"; + } else if (this.x - tooltipWidth/2 < 0){ + this.xAlign = "right"; + } + + if (this.y - tooltipHeight < 0){ + this.yAlign = "below"; + } + + + var tooltipX = this.x - tooltipWidth/2, + tooltipY = this.y - tooltipHeight; + + ctx.fillStyle = this.fillColor; + + switch(this.yAlign) + { + case "above": + //Draw a caret above the x/y + ctx.beginPath(); + ctx.moveTo(this.x,this.y - caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.closePath(); + ctx.fill(); + break; + case "below": + tooltipY = this.y + caretPadding + this.caretHeight; + //Draw a caret below the x/y + ctx.beginPath(); + ctx.moveTo(this.x, this.y + caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.closePath(); + ctx.fill(); + break; + } + + switch(this.xAlign) + { + case "left": + tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); + break; + case "right": + tooltipX = this.x - (this.cornerRadius + this.caretHeight); + break; + } + + drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); + + ctx.fill(); + + ctx.fillStyle = this.textColor; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); } - - } - - function mergeChartConfig(defaults,userDefined){ - var returnObj = {}; - for (var attrname in defaults) { returnObj[attrname] = defaults[attrname]; } - for (var attrname in userDefined) { returnObj[attrname] = userDefined[attrname]; } - return returnObj; - } - - //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ - var cache = {}; - - function tmpl(str, data){ - // Figure out if we're getting a template, or if we need to - // load the template - and be sure to cache the result. - var fn = !/\W/.test(str) ? - cache[str] = cache[str] || - tmpl(document.getElementById(str).innerHTML) : - - // Generate a reusable function that will serve as a template - // generator (and which will be cached). - new Function("obj", - "var p=[],print=function(){p.push.apply(p,arguments);};" + - - // Introduce the data as local variables using with(){} - "with(obj){p.push('" + - - // Convert the template into pure JavaScript - str - .replace(/[\r\t\n]/g, " ") - .split("<%").join("\t") - .replace(/((^|%>)[^\t]*)'/g, "$1\r") - .replace(/\t=(.*?)%>/g, "',$1,'") - .split("\t").join("');") - .split("%>").join("p.push('") - .split("\r").join("\\'") - + "');}return p.join('');"); - - // Provide some basic currying to the user - return data ? fn( data ) : fn; - }; -} + }); + + Chart.MultiTooltip = Chart.Element.extend({ + initialize : function(){ + this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); + + this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; + + this.ctx.font = this.titleFont; + + var titleWidth = this.ctx.measureText(this.title).width, + //Label has a legend square as well so account for this. + labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, + longestTextWidth = max([labelWidth,titleWidth]); + + this.width = longestTextWidth + (this.xPadding*2); + + + var halfHeight = this.height/2; + + //Check to ensure the height will fit on the canvas + //The three is to buffer form the very + if (this.y - halfHeight < 0 ){ + this.y = halfHeight; + } else if (this.y + halfHeight > this.chart.height){ + this.y = this.chart.height - halfHeight; + } + + //Decide whether to align left or right based on position on canvas + if (this.x > this.chart.width/2){ + this.x -= this.xOffset + this.width; + } else { + this.x += this.xOffset; + } + + + }, + getLineHeight : function(index){ + var baseLineHeight = this.y - (this.height/2) + this.yPadding, + afterTitleIndex = index-1; + + //If the index is zero, we're getting the title + if (index === 0){ + return baseLineHeight + this.titleFontSize/2; + } else{ + return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; + } + + }, + draw : function(){ + drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); + var ctx = this.ctx; + ctx.fillStyle = this.fillColor; + ctx.fill(); + ctx.closePath(); + + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.titleTextColor; + ctx.font = this.titleFont; + + ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); + + ctx.font = this.font; + helpers.each(this.labels,function(label,index){ + ctx.fillStyle = this.textColor; + ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); + + //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) + //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + //Instead we'll make a white filled block to put the legendColour palette over. + + ctx.fillStyle = this.legendColorBackground; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + ctx.fillStyle = this.legendColors[index].fill; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + + },this); + } + }); + + Chart.Scale = Chart.Element.extend({ + initialize : function(){ + this.fit(); + }, + buildYLabels : function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + 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; + }, + addXLabel : function(label){ + this.xLabels.push(label); + this.valuesCount++; + this.fit(); + }, + removeXLabel : function(){ + this.xLabels.shift(); + this.valuesCount--; + this.fit(); + }, + // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use + fit: function(){ + // First we need the width of the yLabels, assuming the xLabels aren't rotated + + // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation + this.startPoint = (this.display) ? this.fontSize : 0; + this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels + + // Apply padding settings to the start and end point. + this.startPoint += this.padding; + this.endPoint -= this.padding; + + // Cache the starting height, so can determine if we need to recalculate the scale yAxis + var cachedHeight = this.endPoint - this.startPoint, + cachedYLabelWidth; + + // Build the current yLabels so we have an idea of what size they'll be to start + /* + * This sets what is returned from calculateScaleRange as static properties of this class: + * + this.steps; + this.stepValue; + this.min; + this.max; + * + */ + this.calculateYRange(cachedHeight); + + // With these properties set we can now build the array of yLabels + // and also the width of the largest yLabel + this.buildYLabels(); + + this.calculateXLabelRotation(); + + while((cachedHeight > this.endPoint - this.startPoint)){ + cachedHeight = this.endPoint - this.startPoint; + cachedYLabelWidth = this.yLabelWidth; + + this.calculateYRange(cachedHeight); + this.buildYLabels(); + + // Only go through the xLabel loop again if the yLabel width has changed + if (cachedYLabelWidth < this.yLabelWidth){ + this.calculateXLabelRotation(); + } + } + + }, + calculateXLabelRotation : function(){ + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + + this.ctx.font = this.font; + + var firstWidth = this.ctx.measureText(this.xLabels[0]).width, + lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, + firstRotated, + lastRotated; + + + this.xScalePaddingRight = lastWidth/2 + 3; + this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; + + this.xLabelRotation = 0; + if (this.display){ + var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), + cosRotation, + firstRotatedWidth; + this.xLabelWidth = originalLabelWidth; + //Allow 3 pixels x2 padding either side for label readability + var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; + + //Max label rotate should be 90 - also act as a loop counter + while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ + cosRotation = Math.cos(toRadians(this.xLabelRotation)); + + firstRotated = cosRotation * firstWidth; + lastRotated = cosRotation * lastWidth; + + // We're right aligning the text now. + if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ + this.xScalePaddingLeft = firstRotated + this.fontSize / 2; + } + this.xScalePaddingRight = this.fontSize/2; + + + this.xLabelRotation++; + this.xLabelWidth = cosRotation * originalLabelWidth; + + } + if (this.xLabelRotation > 0){ + this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; + } + } + else{ + this.xLabelWidth = 0; + this.xScalePaddingRight = this.padding; + this.xScalePaddingLeft = this.padding; + } + + }, + // Needs to be overidden in each Chart type + // Otherwise we need to pass all the data into the scale class + calculateYRange: noop, + drawingArea: function(){ + return this.startPoint - this.endPoint; + }, + calculateY : function(value){ + var scalingFactor = this.drawingArea() / (this.min - this.max); + return this.endPoint - (scalingFactor * (value - this.min)); + }, + calculateX : function(index){ + var isRotated = (this.xLabelRotation > 0), + // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, + innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), + valueWidth = innerWidth/(this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), + valueOffset = (valueWidth * index) + this.xScalePaddingLeft; + + if (this.offsetGridLines){ + valueOffset += (valueWidth/2); + } + + return Math.round(valueOffset); + }, + update : function(newProps){ + helpers.extend(this, newProps); + this.fit(); + }, + draw : function(){ + var ctx = this.ctx, + yLabelGap = (this.endPoint - this.startPoint) / this.steps, + xStart = Math.round(this.xScalePaddingLeft); + if (this.display){ + ctx.fillStyle = this.textColor; + ctx.font = this.font; + each(this.yLabels,function(labelString,index){ + var yLabelCenter = this.endPoint - (yLabelGap * index), + linePositionY = Math.round(yLabelCenter); + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + if (this.showLabels){ + ctx.fillText(labelString,xStart - 10,yLabelCenter); + } + ctx.beginPath(); + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + linePositionY += helpers.aliasPixel(ctx.lineWidth); + + ctx.moveTo(xStart, linePositionY); + ctx.lineTo(this.width, linePositionY); + ctx.stroke(); + ctx.closePath(); + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + ctx.beginPath(); + ctx.moveTo(xStart - 5, linePositionY); + ctx.lineTo(xStart, linePositionY); + ctx.stroke(); + ctx.closePath(); + + },this); + + each(this.xLabels,function(label,index){ + var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), + // Check to see if line/bar here and decide where to place the line + linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), + isRotated = (this.xLabelRotation > 0); + + ctx.beginPath(); + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.startPoint - 3); + ctx.stroke(); + ctx.closePath(); + + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + + + // Small lines at the bottom of the base grid line + ctx.beginPath(); + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.endPoint + 5); + ctx.stroke(); + ctx.closePath(); + + ctx.save(); + ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); + ctx.rotate(toRadians(this.xLabelRotation)*-1); + ctx.font = this.font; + ctx.textAlign = (isRotated) ? "right" : "center"; + ctx.textBaseline = (isRotated) ? "middle" : "top"; + ctx.fillText(label, 0, 0); + ctx.restore(); + },this); + + } + } + + }); + + Chart.RadialScale = Chart.Element.extend({ + initialize: function(){ + this.size = min([this.height, this.width]); + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + }, + calculateCenterOffset: function(value){ + // Take into account half font size + the yPadding of the top value + var scalingFactor = this.drawingArea / (this.max - this.min); + + return (value - this.min) * scalingFactor; + }, + update : function(){ + if (!this.lineArc){ + this.setScaleSize(); + } else { + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + } + this.buildYLabels(); + }, + buildYLabels: function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + }, + getCircumference : function(){ + return ((Math.PI*2) / this.valuesCount); + }, + setScaleSize: function(){ + /* + * Right, this is really confusing and there is a lot of maths going on here + * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + * + * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + * + * Solution: + * + * We assume the radius of the polygon is half the size of the canvas at first + * at each index we check if the text overlaps. + * + * Where it does, we store that angle and that index. + * + * After finding the largest index and angle we calculate how much we need to remove + * from the shape radius to move the point inwards by that x. + * + * We average the left and right distances to get the maximum shape radius that can fit in the box + * along with labels. + * + * Once we have that, we can find the centre point for the chart, by taking the x text protrusion + * on each side, removing that from the size, halving it and adding the left x protrusion width. + * + * This will mean we have a shape fitted to the canvas, as large as it can be with the labels + * and position it in the most space efficient manner + * + * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + */ + + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), + pointPosition, + i, + textWidth, + halfTextWidth, + furthestRight = this.width, + furthestRightIndex, + furthestRightAngle, + furthestLeft = 0, + furthestLeftIndex, + furthestLeftAngle, + xProtrusionLeft, + xProtrusionRight, + radiusReductionRight, + radiusReductionLeft, + maxWidthRadius; + this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + for (i=0;i furthestRight) { + furthestRight = pointPosition.x + halfTextWidth; + furthestRightIndex = i; + } + if (pointPosition.x - halfTextWidth < furthestLeft) { + furthestLeft = pointPosition.x - halfTextWidth; + furthestLeftIndex = i; + } + } + else if (i < this.valuesCount/2) { + // Less than half the values means we'll left align the text + if (pointPosition.x + textWidth > furthestRight) { + furthestRight = pointPosition.x + textWidth; + furthestRightIndex = i; + } + } + else if (i > this.valuesCount/2){ + // More than half the values means we'll right align the text + if (pointPosition.x - textWidth < furthestLeft) { + furthestLeft = pointPosition.x - textWidth; + furthestLeftIndex = i; + } + } + } + + xProtrusionLeft = furthestLeft; + + xProtrusionRight = Math.ceil(furthestRight - this.width); + + furthestRightAngle = this.getIndexAngle(furthestRightIndex); + + furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); + + radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); + + radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); + + // Ensure we actually need to reduce the size of the chart + radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; + radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; + + this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; + + //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) + this.setCenterPoint(radiusReductionLeft, radiusReductionRight); + + }, + setCenterPoint: function(leftMovement, rightMovement){ + + var maxRight = this.width - rightMovement - this.drawingArea, + maxLeft = leftMovement + this.drawingArea; + + this.xCenter = (maxLeft + maxRight)/2; + // Always vertically in the centre as the text height doesn't change + this.yCenter = (this.height/2); + }, + + getIndexAngle : function(index){ + var angleMultiplier = (Math.PI * 2) / this.valuesCount; + // Start from the top instead of right, so remove a quarter of the circle + + return index * angleMultiplier - (Math.PI/2); + }, + getPointPosition : function(index, distanceFromCenter){ + var thisAngle = this.getIndexAngle(index); + return { + x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, + y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter + }; + }, + draw: function(){ + if (this.display){ + var ctx = this.ctx; + each(this.yLabels, function(label, index){ + // Don't draw a centre value + if (index > 0){ + var yCenterOffset = index * (this.drawingArea/this.steps), + yHeight = this.yCenter - yCenterOffset, + pointPosition; + + // Draw circular lines around the scale + if (this.lineWidth > 0){ + ctx.strokeStyle = this.lineColor; + ctx.lineWidth = this.lineWidth; + + if(this.lineArc){ + ctx.beginPath(); + ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); + ctx.closePath(); + ctx.stroke(); + } else{ + ctx.beginPath(); + for (var i=0;i= 0; i--) { + if (this.angleLineWidth > 0){ + var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); + ctx.beginPath(); + ctx.moveTo(this.xCenter, this.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); + ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + ctx.fillStyle = this.pointLabelFontColor; + + var labelsCount = this.labels.length, + halfLabelsCount = this.labels.length/2, + quarterLabelsCount = halfLabelsCount/2, + upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), + exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); + if (i === 0){ + ctx.textAlign = 'center'; + } else if(i === halfLabelsCount){ + ctx.textAlign = 'center'; + } else if (i < halfLabelsCount){ + ctx.textAlign = 'left'; + } else { + ctx.textAlign = 'right'; + } + + // Set the correct text baseline based on outer positioning + if (exactQuarter){ + ctx.textBaseline = 'middle'; + } else if (upperHalf){ + ctx.textBaseline = 'bottom'; + } else { + ctx.textBaseline = 'top'; + } + + ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); + } + } + } + } + }); + + // 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. + var timeout; + return function(){ + clearTimeout(timeout); + timeout = setTimeout(function(){ + each(Chart.instances,function(instance){ + // If the responsive flag is set in the chart instance config + // Cascade the resize event down to the chart. + if (instance.options.responsive){ + instance.resize(instance.render, true); + } + }); + }, 50); + }; + })()); + + + if (amd) { + define(function(){ + return Chart; + }); + } + + root.Chart = Chart; + + Chart.noConflict = function(){ + root.Chart = previous; + return Chart; + }; + +}).call(this); +(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 - 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 : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }; + + + 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){ + if (helpers.isNumber(dataPoint)){ + //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], + 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, + 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){ + if (helpers.isNumber(value)){ + //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, + 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){ + 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); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Whether we should show a stroke on each segment + segmentShowStroke : true, + + //String - The colour of each segment stroke + segmentStrokeColor : "#fff", + + //Number - The width of each segment stroke + segmentStrokeWidth : 2, + + //The percentage of the chart that we cut out of the middle. + percentageInnerCutout : 50, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect + animationEasing : "easeOutBounce", + + //Boolean - Whether we animate the rotation of the Doughnut + animateRotate : true, + + //Boolean - Whether we animate scaling the Doughnut from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "Doughnut", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + + //Declare segments as a static property to prevent inheriting across the Chart type prototype + this.segments = []; + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + + this.SegmentArc = Chart.Arc.extend({ + ctx : this.chart.ctx, + x : this.chart.width/2, + y : this.chart.height/2 + }); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + this.calculateTotal(data); + + helpers.each(data,function(datapoint, index){ + this.addData(datapoint, index, true); + },this); + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + this.segments.splice(index, 0, new this.SegmentArc({ + value : segment.value, + outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, + innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, + fillColor : segment.color, + highlightColor : segment.highlight || segment.color, + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + startAngle : Math.PI * 1.5, + circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), + label : segment.label + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + calculateCircumference : function(value){ + return (Math.PI*2)*(value / this.total); + }, + calculateTotal : function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += segment.value; + },this); + }, + update : function(){ + this.calculateTotal(this.segments); + + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor']); + }); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + this.render(); + }, + + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + }); + }, this); + }, + draw : function(easeDecimal){ + var animDecimal = (easeDecimal) ? easeDecimal : 1; + this.clear(); + helpers.each(this.segments,function(segment,index){ + segment.transition({ + circumference : this.calculateCircumference(segment.value), + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + },animDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + segment.draw(); + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length-1){ + this.segments[index+1].startAngle = segment.endAngle; + } + },this); + + } + }); + + Chart.types.Doughnut.extend({ + name : "Pie", + defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) + }); + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + + ///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 the line is curved between points + bezierCurve : true, + + //Number - Tension of the bezier curve between points + bezierCurveTension : 0.4, + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 4, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + name: "Line", + defaults : defaultConfig, + 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({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx, + inRange : function(mouseX){ + return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePoints, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + this.showTooltip(activePoints); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + + helpers.each(dataset.data,function(dataPoint,index){ + //Best way to do this? or in draw sequence...? + if (helpers.isNumber(dataPoint)){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + // x: this.scale.calculateX(index), + // y: this.scale.endPoint, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + } + },this); + + this.buildScale(data.labels); + + + this.eachPoints(function(point, index){ + helpers.extend(point, { + x: this.scale.calculateX(index), + y: this.scale.endPoint + }); + point.save(); + }, this); + + },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.eachPoints(function(point){ + point.save(); + }); + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + getPointsAtEvent : function(e){ + var pointsArray = [], + eventPosition = helpers.getRelativePosition(e); + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,function(point){ + if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); + }); + },this); + return pointsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachPoints(function(point){ + values.push(point.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, + 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.pointDotRadius + this.options.pointDotStrokeWidth, + 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 Chart.Scale(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + + helpers.each(valuesArray,function(value,datasetIndex){ + if (helpers.isNumber(value)){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: this.scale.calculateX(this.scale.valuesCount+1), + y: this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + } + },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.points.shift(); + },this); + this.update(); + }, + reflow : function(){ + 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); + + + helpers.each(this.datasets,function(dataset){ + + //Transition each point first so that the line and point drawing isn't out of sync + //We can use this extra loop to calculate the control points of this dataset also in this loop + + helpers.each(dataset.points,function(point,index){ + point.transition({ + y : this.scale.calculateY(point.value), + x : this.scale.calculateX(index) + }, easingDecimal); + + },this); + + + // Control points need to be calculated in a seperate 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(dataset.points,function(point,index){ + //If we're at the start or end, we don't have a previous/next point + //By setting the tension to 0 here, the curve will transition to straight at the end + if (index === 0){ + point.controlPoints = helpers.splineCurve(point,point,dataset.points[index+1],0); + } + else if (index >= dataset.points.length-1){ + point.controlPoints = helpers.splineCurve(dataset.points[index-1],point,point,0); + } + else{ + point.controlPoints = helpers.splineCurve(dataset.points[index-1],point,dataset.points[index+1],this.options.bezierCurveTension); + } + },this); + } + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + helpers.each(dataset.points,function(point,index){ + if (index>0){ + if(this.options.bezierCurve){ + ctx.bezierCurveTo( + dataset.points[index-1].controlPoints.outer.x, + dataset.points[index-1].controlPoints.outer.y, + point.controlPoints.inner.x, + point.controlPoints.inner.y, + point.x, + point.y + ); + } + else{ + ctx.lineTo(point.x,point.y); + } + + } + else{ + ctx.moveTo(point.x,point.y); + } + },this); + ctx.stroke(); + + + if (this.options.datasetFill){ + //Round off the line by going to the base of the chart, back to the start, then fill. + ctx.lineTo(dataset.points[dataset.points.length-1].x, this.scale.endPoint); + ctx.lineTo(this.scale.calculateX(0), this.scale.endPoint); + ctx.fillStyle = dataset.fillColor; + ctx.closePath(); + ctx.fill(); + } + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(dataset.points,function(point){ + point.draw(); + }); + + },this); + } + }); + + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Show a backdrop to the scale label + scaleShowLabelBackdrop : true, + + //String - The colour of the label backdrop + scaleBackdropColor : "rgba(255,255,255,0.75)", + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //Number - The backdrop padding above & below the label in pixels + scaleBackdropPaddingY : 2, + + //Number - The backdrop padding to the side of the label in pixels + scaleBackdropPaddingX : 2, + + //Boolean - Show line for each value in the scale + scaleShowLine : true, + + //Boolean - Stroke a line around each segment in the chart + segmentShowStroke : true, + + //String - The colour of the stroke on each segement. + segmentStrokeColor : "#fff", + + //Number - The width of the stroke value in pixels + segmentStrokeWidth : 2, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect. + animationEasing : "easeOutBounce", + + //Boolean - Whether to animate the rotation of the chart + animateRotate : true, + + //Boolean - Whether to animate scaling the chart from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "PolarArea", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + this.segments = []; + //Declare segment class as a chart instance specific class, so it can share props for this instance + this.SegmentArc = Chart.Arc.extend({ + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + ctx : this.chart.ctx, + innerRadius : 0, + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + lineArc: true, + width: this.chart.width, + height: this.chart.height, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + valuesCount: data.length + }); + + this.updateScaleRange(data); + + this.scale.update(); + + helpers.each(data,function(segment,index){ + this.addData(segment,index,true); + },this); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + + this.segments.splice(index, 0, new this.SegmentArc({ + fillColor: segment.color, + highlightColor: segment.highlight || segment.color, + label: segment.label, + value: segment.value, + outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), + circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), + startAngle: Math.PI * 1.5 + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + calculateTotal: function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += segment.value; + },this); + this.scale.valuesCount = this.segments.length; + }, + updateScaleRange: function(datapoints){ + var valuesArray = []; + helpers.each(datapoints,function(segment){ + valuesArray.push(segment.value); + }); + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes, + { + size: helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + } + ); + + }, + update : function(){ + this.calculateTotal(this.segments); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + this.render(); + }, + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.updateScaleRange(this.segments); + this.scale.update(); + + helpers.extend(this.scale,{ + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.scale.calculateCenterOffset(segment.value) + }); + }, this); + + }, + draw : function(ease){ + var easingDecimal = ease || 1; + //Clear & draw the canvas + this.clear(); + helpers.each(this.segments,function(segment, index){ + segment.transition({ + circumference : this.scale.getCircumference(), + outerRadius : this.scale.calculateCenterOffset(segment.value) + },easingDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + // If we've removed the first segment we need to set the first one to + // start at the top. + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length - 1){ + this.segments[index+1].startAngle = segment.endAngle; + } + segment.draw(); + }, this); + this.scale.draw(); + } + }); + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + + Chart.Type.extend({ + name: "Radar", + defaults:{ + //Boolean - Whether to show lines for each scale point + scaleShowLine : true, + + //Boolean - Whether we show the angle lines out of the radar + angleShowLineOut : true, + + //Boolean - Whether to show labels on the scale + scaleShowLabels : false, + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //String - Colour of the angle line + angleLineColor : "rgba(0,0,0,.1)", + + //Number - Pixel width of the angle line + angleLineWidth : 1, + + //String - Point label font declaration + pointLabelFontFamily : "'Arial'", + + //String - Point label font weight + pointLabelFontStyle : "normal", + + //Number - Point label font size in pixels + pointLabelFontSize : 10, + + //String - Point label font colour + pointLabelFontColor : "#666", + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 3, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }, + + initialize: function(data){ + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx + }); + + this.datasets = []; + + this.buildScale(data); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePointsCollection, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + + this.showTooltip(activePointsCollection); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label: dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Best way to do this? or in draw sequence...? + if (helpers.isNumber(dataPoint)){ + //Add a new point for each piece of data, passing any required data to draw. + var pointPosition; + if (!this.scale.animation){ + pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); + } + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, + y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + } + },this); + + },this); + + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + + getPointsAtEvent : function(evt){ + var mousePosition = helpers.getRelativePosition(evt), + fromCenter = helpers.getAngleFromPoint({ + x: this.scale.xCenter, + y: this.scale.yCenter + }, mousePosition); + + var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, + pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), + activePointsCollection = []; + + // If we're at the top, make the pointIndex 0 to get the first of the array. + if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ + pointIndex = 0; + } + + if (fromCenter.distance <= this.scale.drawingArea){ + helpers.each(this.datasets, function(dataset){ + activePointsCollection.push(dataset.points[pointIndex]); + }); + } + + return activePointsCollection; + }, + + buildScale : function(data){ + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + angleLineColor : this.options.angleLineColor, + angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, + // Point labels at the edge of each line + pointLabelFontColor : this.options.pointLabelFontColor, + pointLabelFontSize : this.options.pointLabelFontSize, + pointLabelFontFamily : this.options.pointLabelFontFamily, + pointLabelFontStyle : this.options.pointLabelFontStyle, + height : this.chart.height, + width: this.chart.width, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + labels: data.labels, + valuesCount: data.datasets[0].data.length + }); + + this.scale.setScaleSize(); + this.updateScaleRange(data.datasets); + this.scale.buildYLabels(); + }, + updateScaleRange: function(datasets){ + var valuesArray = (function(){ + var totalDataArray = []; + helpers.each(datasets,function(dataset){ + if (dataset.data){ + totalDataArray = totalDataArray.concat(dataset.data); + } + else { + helpers.each(dataset.points, function(point){ + totalDataArray.push(point.value); + }); + } + }); + return totalDataArray; + })(); + + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes + ); + + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + this.scale.valuesCount++; + helpers.each(valuesArray,function(value,datasetIndex){ + if (helpers.isNumber(value)){ + var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: pointPosition.x, + y: pointPosition.y, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + } + },this); + + this.scale.labels.push(label); + + this.reflow(); + + this.update(); + }, + removeData : function(){ + this.scale.valuesCount--; + this.scale.labels.shift(); + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.reflow(); + this.update(); + }, + update : function(){ + this.eachPoints(function(point){ + point.save(); + }); + this.render(); + }, + reflow: function(){ + helpers.extend(this.scale, { + width : this.chart.width, + height: this.chart.height, + size : helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + this.updateScaleRange(this.datasets); + this.scale.setScaleSize(); + this.scale.buildYLabels(); + }, + draw : function(ease){ + var easeDecimal = ease || 1, + ctx = this.chart.ctx; + this.clear(); + this.scale.draw(); + + helpers.each(this.datasets,function(dataset){ + + //Transition each point first so that the line and point drawing isn't out of sync + helpers.each(dataset.points,function(point,index){ + point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); + },this); + + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + helpers.each(dataset.points,function(point,index){ + if (index === 0){ + ctx.moveTo(point.x,point.y); + } + else{ + ctx.lineTo(point.x,point.y); + } + },this); + ctx.closePath(); + ctx.stroke(); + + ctx.fillStyle = dataset.fillColor; + ctx.fill(); + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(dataset.points,function(point){ + point.draw(); + }); + + },this); + + } + + }); + + + +}).call(this); \ No newline at end of file diff --git a/Chart.min.js b/Chart.min.js index ab635881087..4e3e59079a0 100644 --- a/Chart.min.js +++ b/Chart.min.js @@ -1,39 +1,10 @@ -var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a= -Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);dc;)a=dc?c:!isNaN(parseFloat(b))&& -isFinite(b)&&a)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c? -b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)? -0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1== -a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);ea?-0.5*e*Math.pow(2,10* -(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)* -a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0, -scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce", -animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)", -scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a, -c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1, -onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0, -pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'", -scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]); -d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;fe&&(e=a[f].value),a[f].valuel&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE; -h=Number.MAX_VALUE;for(f=0;fe&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;gt?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0t?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]< -h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;ed?h:d;d+=10}r=q-d-t;m= -Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,showTooltips:!0,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;ni)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),C=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),y=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=C(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}var s={};return e(t,i)}),b=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=y(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),st?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-b.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*b.easeInBounce(2*t):.5*b.easeOutBounce(2*t-1)+.5}}),w=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=(s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),s.animationLoop=function(t,i,e,s,n,o){var a=0,h=b[e]||b.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=w(l):n.apply(o)};w(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),L=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},k=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},P(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){L(t.chart.canvas,e,i)})}),F=s.getMaximumSize=function(t){var i=t.parentNode;return i.clientWidth},R=s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))},A=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},T=s.fontString=function(t,i,e){return i+" "+t+"px "+e},M=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},W=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return A(this.chart),this},stop:function(){return s.cancelAnimFrame.call(t,this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=F(this.chart.canvas),s=e/this.chart.aspectRatio;return i.width=this.chart.width=e,i.height=this.chart.height=s,R(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return y(this.options.legendTemplate,this)},destroy:function(){this.clear(),k(this,this.events),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:y(this.options.tooltipTemplate,t),chart:this.chart}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}}}),e.Element.extend=r,e.Point=e.Element.extend({inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=T(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;switch(t.fillStyle=this.fillColor,this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}W(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=T(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=T(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=M(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){W(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(y(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?M(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),tthis.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=M(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/(this.valuesCount-(this.offsetGridLines?0:1)),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a);t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath(),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0;t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath(),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(y(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=T(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;ip&&(p=t.x+s,n=i),t.x-sp&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=T(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p&&define(function(){return e}),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'
    <% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(n,o){e.isNumber(n)&&s.bars.push(new this.BarClass({value:n,label:t.labels[o],strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<% for (var i=0; i
  • <%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[]; +e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(t/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)=t.points.length-1?e.splineCurve(t.points[s-1],i,i,0):e.splineCurve(t.points[s-1],i,t.points[s+1],this.options.bezierCurveTension)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(i,e){e>0?this.options.bezierCurve?s.bezierCurveTo(t.points[e-1].controlPoints.outer.x,t.points[e-1].controlPoints.outer.y,i.controlPoints.inner.x,i.controlPoints.inner.y,i.x,i.y):s.lineTo(i.x,i.y):s.moveTo(i.x,i.y)},this),s.stroke(),this.options.datasetFill&&(s.lineTo(t.points[t.points.length-1].x,this.scale.endPoint),s.lineTo(this.scale.calculateX(0),this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(t.points,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'
      <% for (var i=0; i
    • <%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    '};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(n,o){if(e.isNumber(n)){var a;this.scale.animation||(a=this.scale.getPointPosition(o,this.scale.calculateCenterOffset(n))),s.points.push(new this.PointClass({value:n,label:t.labels[o],x:this.options.animation?this.scale.xCenter:a.x,y:this.options.animation?this.scale.yCenter:a.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))}},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,s){if(e.isNumber(t)){var n=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[s].points.push(new this.PointClass({value:t,label:i,x:n.x,y:n.y,strokeColor:this.datasets[s].pointStrokeColor,fillColor:this.datasets[s].pointColor}))}},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.draw()})},this)}})}.call(this); \ No newline at end of file diff --git a/src/Chart.Bar.js b/src/Chart.Bar.js new file mode 100644 index 00000000000..7a480095b4b --- /dev/null +++ b/src/Chart.Bar.js @@ -0,0 +1,295 @@ +(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 - 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 : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " + + }; + + + 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){ + if (helpers.isNumber(dataPoint)){ + //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], + 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, + 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){ + if (helpers.isNumber(value)){ + //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, + 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){ + 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); \ No newline at end of file diff --git a/src/Chart.Core.js b/src/Chart.Core.js new file mode 100755 index 00000000000..fb7a4957d7e --- /dev/null +++ b/src/Chart.Core.js @@ -0,0 +1,1883 @@ +/*! + * Chart.js + * http://chartjs.org/ + * + * Copyright 2014 Nick Downie + * Released under the MIT license + * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md + */ + + +(function(){ + + "use strict"; + + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart; + + //Occupy the global variable of Chart, and create a simple base class + var Chart = function(context){ + var chart = this; + this.canvas = context.canvas; + + this.ctx = context; + + //Variables global to the chart + var width = this.width = context.canvas.width; + var 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); + + return this; + }; + //Globally expose the defaults to allow for user updating/changing + Chart.defaults = { + global: { + // Boolean - Whether to animate the chart + animation: true, + + // Number - Number of animation steps + animationSteps: 60, + + // String - Animation easing effect + animationEasing: "easeOutQuart", + + // Boolean - If we should show the scale at all + showScale: true, + + // Boolean - If we want to override with a hard coded scale + scaleOverride: false, + + // ** Required if scaleOverride is true ** + // Number - The number of steps in a hard coded scale + scaleSteps: null, + // Number - The value jump in the hard coded scale + scaleStepWidth: null, + // Number - The scale starting value + scaleStartValue: null, + + // String - Colour of the scale line + scaleLineColor: "rgba(0,0,0,.1)", + + // Number - Pixel width of the scale line + scaleLineWidth: 1, + + // Boolean - Whether to show labels on the scale + scaleShowLabels: true, + + // Interpolated JS string - can access value + scaleLabel: "<%=value%>", + + // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there + scaleIntegersOnly: true, + + // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero: false, + + // String - Scale label font declaration for the scale label + scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Scale label font size in pixels + scaleFontSize: 12, + + // String - Scale label font weight style + scaleFontStyle: "normal", + + // String - Scale label font colour + scaleFontColor: "#666", + + // Boolean - whether or not the chart should be responsive and resize when the browser does. + responsive: false, + + // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove + showTooltips: true, + + // Array - Array of string names to attach tooltip events + tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], + + // String - Tooltip background colour + tooltipFillColor: "rgba(0,0,0,0.8)", + + // String - Tooltip label font declaration for the scale label + tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip label font size in pixels + tooltipFontSize: 14, + + // String - Tooltip font weight style + tooltipFontStyle: "normal", + + // String - Tooltip label font colour + tooltipFontColor: "#fff", + + // String - Tooltip title font declaration for the scale label + tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip title font size in pixels + tooltipTitleFontSize: 14, + + // String - Tooltip title font weight style + tooltipTitleFontStyle: "bold", + + // String - Tooltip title font colour + tooltipTitleFontColor: "#fff", + + // Number - pixel width of padding around tooltip text + tooltipYPadding: 6, + + // Number - pixel width of padding around tooltip text + tooltipXPadding: 6, + + // Number - Size of the caret on the tooltip + tooltipCaretSize: 8, + + // Number - Pixel radius of the tooltip border + tooltipCornerRadius: 6, + + // Number - Pixel offset from point x to tooltip edge + tooltipXOffset: 10, + + // String - Template string for single tooltips + tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", + + // String - Template string for single tooltips + multiTooltipTemplate: "<%= value %>", + + // String - Colour behind the legend colour block + multiTooltipKeyBackground: '#fff', + + // Function - Will fire on animation progression. + onAnimationProgress: function(){}, + + // Function - Will fire on animation completion. + onAnimationComplete: function(){} + + } + }; + + //Create a dictionary of chart types, to allow for extension of existing types + Chart.types = {}; + + //Global Chart helpers object for utility methods and classes + var helpers = Chart.helpers = {}; + + //-- Basic js utility methods + var each = helpers.each = function(loopable,callback,self){ + var additionalArgs = Array.prototype.slice.call(arguments, 3); + // Check to see if null or undefined firstly. + if (loopable){ + if (loopable.length === +loopable.length){ + var i; + for (i=0; i maxValue ) { + return maxValue; + } + } + else if(isNumber(minValue)){ + if ( valueToCap < minValue ){ + return minValue; + } + } + return valueToCap; + }, + getDecimalPlaces = helpers.getDecimalPlaces = function(num){ + if (num%1!==0 && isNumber(num)){ + return num.toString().split(".")[1].length; + } + else { + return 0; + } + }, + toRadians = helpers.radians = function(degrees){ + return degrees * (Math.PI/180); + }, + // Gets the angle from vertical upright to the point about a centre. + getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ + var distanceFromXCenter = anglePoint.x - centrePoint.x, + distanceFromYCenter = anglePoint.y - centrePoint.y, + radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + + + var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); + + //If the segment is in the top left quadrant, we need to add another rotation to the angle + if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ + angle += Math.PI*2; + } + + return { + angle: angle, + distance: radialDistanceFromCenter + }; + }, + aliasPixel = helpers.aliasPixel = function(pixelWidth){ + return (pixelWidth % 2 === 0) ? 0 : 0.5; + }, + splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ + //Props to Rob Spencer at scaled innovation for his post on splining between points + //http://scaledinnovation.com/analytics/splines/aboutSplines.html + var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), + d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), + fa=t*d01/(d01+d12),// scaling factor for triangle Ta + fb=t*d12/(d01+d12); + return { + inner : { + x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) + }, + outer : { + x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) + } + }; + }, + calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ + return Math.floor(Math.log(val) / Math.LN10); + }, + calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ + + //Set a minimum step of two - a point at the top of the graph, and a point at the base + var minSteps = 2, + maxSteps = Math.floor(drawingSize/(textSize * 1.5)), + skipFitting = (minSteps >= maxSteps); + + 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 + // Adding/minusing 0.5 will give us a range of 1. + if (maxValue === minValue){ + maxValue += 0.5; + // So we don't end up with a graph with a negative start value if we've said always start from zero + if (minValue >= 0.5 && !startFromZero){ + minValue -= 0.5; + } + else{ + // Make up a whole number above the values + maxValue += 0.5; + } + } + + var valueRange = Math.abs(maxValue - minValue), + rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), + graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphRange = graphMax - graphMin, + stepValue = Math.pow(10, rangeOrderOfMagnitude), + numberOfSteps = Math.round(graphRange / stepValue); + + //If we have more space on the graph we'll use it to give more definition to the data + while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { + if(numberOfSteps > maxSteps){ + stepValue *=2; + numberOfSteps = Math.round(graphRange/stepValue); + // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. + if (numberOfSteps % 1 !== 0){ + skipFitting = true; + } + } + //We can fit in double the amount of scale points on the scale + else{ + //If user has declared ints only, and the step value isn't a decimal + if (integersOnly && rangeOrderOfMagnitude >= 0){ + //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float + if(stepValue/2 % 1 === 0){ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + //If it would make it a float break out of the loop + else{ + break; + } + } + //If the scale doesn't have to be an int, make the scale more granular anyway. + else{ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + + } + } + + if (skipFitting){ + numberOfSteps = minSteps; + stepValue = graphRange / numberOfSteps; + } + + return { + steps : numberOfSteps, + stepValue : stepValue, + min : graphMin, + max : graphMin + (numberOfSteps * stepValue) + }; + + }, + /* jshint ignore:start */ + // Blows up jshint errors based on the new Function constructor + //Templating methods + //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ + template = helpers.template = function(templateString, valuesObject){ + var cache = {}; + function tmpl(str, data){ + // Figure out if we're getting a template, or if we need to + // load the template - and be sure to cache the result. + var fn = !/\W/.test(str) ? + cache[str] = cache[str] : + + // Generate a reusable function that will serve as a template + // generator (and which will be cached). + new Function("obj", + "var p=[],print=function(){p.push.apply(p,arguments);};" + + + // Introduce the data as local variables using with(){} + "with(obj){p.push('" + + + // Convert the template into pure JavaScript + str + .replace(/[\r\t\n]/g, " ") + .split("<%").join("\t") + .replace(/((^|%>)[^\t]*)'/g, "$1\r") + .replace(/\t=(.*?)%>/g, "',$1,'") + .split("\t").join("');") + .split("%>").join("p.push('") + .split("\r").join("\\'") + + "');}return p.join('');" + ); + + // Provide some basic currying to the user + return data ? fn( data ) : fn; + } + return tmpl(templateString,valuesObject); + }, + /* jshint ignore:end */ + generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ + var labelsArray = new Array(numberOfSteps); + if (labelTemplateString){ + each(labelsArray,function(val,index){ + labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); + }); + } + return labelsArray; + }, + //--Animation methods + //Easing functions adapted from Robert Penner's easing equations + //http://www.robertpenner.com/easing/ + easingEffects = helpers.easingEffects = { + linear: function (t) { + return t; + }, + easeInQuad: function (t) { + return t * t; + }, + easeOutQuad: function (t) { + return -1 * t * (t - 2); + }, + easeInOutQuad: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; + return -1 / 2 * ((--t) * (t - 2) - 1); + }, + easeInCubic: function (t) { + return t * t * t; + }, + easeOutCubic: function (t) { + return 1 * ((t = t / 1 - 1) * t * t + 1); + }, + easeInOutCubic: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; + return 1 / 2 * ((t -= 2) * t * t + 2); + }, + easeInQuart: function (t) { + return t * t * t * t; + }, + easeOutQuart: function (t) { + 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; + return -1 / 2 * ((t -= 2) * t * t * t - 2); + }, + easeInQuint: function (t) { + return 1 * (t /= 1) * t * t * t * t; + }, + easeOutQuint: function (t) { + 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; + return 1 / 2 * ((t -= 2) * t * t * t * t + 2); + }, + easeInSine: function (t) { + return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; + }, + easeOutSine: function (t) { + return 1 * Math.sin(t / 1 * (Math.PI / 2)); + }, + easeInOutSine: function (t) { + return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); + }, + easeInExpo: function (t) { + return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); + }, + easeOutExpo: function (t) { + 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)); + return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); + }, + easeInCirc: function (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); + 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 (a < Math.abs(1)) { + a = 1; + s = p / 4; + } 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 (a < Math.abs(1)) { + a = 1; + s = p / 4; + } 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 (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)); + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; + }, + easeInBack: function (t) { + var s = 1.70158; + return 1 * (t /= 1) * t * ((s + 1) * t - s); + }, + easeOutBack: function (t) { + var s = 1.70158; + return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); + }, + easeInOutBack: function (t) { + var s = 1.70158; + 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) { + return 1 - easingEffects.easeOutBounce(1 - t); + }, + easeOutBounce: function (t) { + if ((t /= 1) < (1 / 2.75)) { + return 1 * (7.5625 * t * t); + } else if (t < (2 / 2.75)) { + return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); + } else if (t < (2.5 / 2.75)) { + return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); + } else { + return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); + } + }, + easeInOutBounce: function (t) { + if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; + return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; + } + }, + //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ + requestAnimFrame = helpers.requestAnimFrame = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, 1000 / 60); + }; + })(), + cancelAnimFrame = helpers.cancelAnimFrame = (function(){ + return window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.mozCancelAnimationFrame || + window.oCancelAnimationFrame || + window.msCancelAnimationFrame || + function(callback) { + return window.clearTimeout(callback, 1000 / 60); + }; + })(), + animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ + + var currentStep = 0, + easingFunction = easingEffects[easingString] || easingEffects.linear; + + var animationFrame = function(){ + currentStep++; + var stepDecimal = currentStep/totalSteps; + var easeDecimal = easingFunction(stepDecimal); + + callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); + onProgress.call(chartInstance,easeDecimal,stepDecimal); + if (currentStep < totalSteps){ + chartInstance.animationFrame = requestAnimFrame(animationFrame); + } else{ + onComplete.apply(chartInstance); + } + }; + requestAnimFrame(animationFrame); + }, + //-- DOM methods + getRelativePosition = helpers.getRelativePosition = function(evt){ + var mouseX, mouseY; + var e = evt.originalEvent || evt, + canvas = evt.currentTarget || evt.srcElement, + boundingRect = canvas.getBoundingClientRect(); + + if (e.touches){ + mouseX = e.touches[0].clientX - boundingRect.left; + mouseY = e.touches[0].clientY - boundingRect.top; + + } + else{ + mouseX = e.clientX - boundingRect.left; + mouseY = e.clientY - boundingRect.top; + } + + return { + x : mouseX, + y : mouseY + }; + + }, + addEvent = helpers.addEvent = function(node,eventType,method){ + if (node.addEventListener){ + node.addEventListener(eventType,method); + } else if (node.attachEvent){ + node.attachEvent("on"+eventType, method); + } else { + node["on"+eventType] = method; + } + }, + removeEvent = helpers.removeEvent = function(node, eventType, handler){ + if (node.removeEventListener){ + node.removeEventListener(eventType, handler, false); + } else if (node.detachEvent){ + node.detachEvent("on"+eventType,handler); + } else{ + node["on" + eventType] = noop; + } + }, + bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ + // Create the events object if it's not already present + if (!chartInstance.events) chartInstance.events = {}; + + each(arrayOfEvents,function(eventName){ + chartInstance.events[eventName] = function(){ + handler.apply(chartInstance, arguments); + }; + addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); + }); + }, + unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { + each(arrayOfEvents, function(handler,eventName){ + removeEvent(chartInstance.chart.canvas, eventName, handler); + }); + }, + getMaximumSize = helpers.getMaximumSize = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientWidth; + }, + retinaScale = helpers.retinaScale = function(chart){ + var ctx = chart.ctx, + width = chart.canvas.width, + height = chart.canvas.height; + //console.log(width + " x " + height); + if (window.devicePixelRatio) { + ctx.canvas.style.width = width + "px"; + ctx.canvas.style.height = height + "px"; + ctx.canvas.height = height * window.devicePixelRatio; + ctx.canvas.width = width * window.devicePixelRatio; + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + } + }, + //-- Canvas methods + clear = helpers.clear = function(chart){ + chart.ctx.clearRect(0,0,chart.width,chart.height); + }, + fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ + return fontStyle + " " + pixelSize+"px " + fontFamily; + }, + longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ + ctx.font = font; + var longest = 0; + each(arrayOfStrings,function(string){ + var textWidth = ctx.measureText(string).width; + longest = (textWidth > longest) ? textWidth : longest; + }); + return longest; + }, + drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + }; + + + //Store a reference to each instance - allowing us to globally resize chart instances on window resize. + //Destroy method on the chart will remove the instance of the chart from this reference. + Chart.instances = {}; + + Chart.Type = function(data,options,chart){ + this.options = options; + this.chart = chart; + this.id = uid(); + //Add the chart instance to the global namespace + Chart.instances[this.id] = this; + + // Initialize is always called when a chart type is created + // By default it is a no op, but it should be extended + if (options.responsive){ + this.resize(); + } + this.initialize.call(this,data); + }; + + //Core methods that'll be a part of every chart type + extend(Chart.Type.prototype,{ + initialize : function(){return this;}, + clear : function(){ + clear(this.chart); + return this; + }, + stop : function(){ + // Stops any current animation loop occuring + helpers.cancelAnimFrame.call(root, this.animationFrame); + return this; + }, + resize : function(callback){ + this.stop(); + var canvas = this.chart.canvas, + newWidth = getMaximumSize(this.chart.canvas), + newHeight = newWidth / this.chart.aspectRatio; + + canvas.width = this.chart.width = newWidth; + canvas.height = this.chart.height = newHeight; + + retinaScale(this.chart); + + if (typeof callback === "function"){ + callback.apply(this, Array.prototype.slice.call(arguments, 1)); + } + return this; + }, + reflow : noop, + render : function(reflow){ + 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 + ); + } + else{ + this.draw(); + this.options.onAnimationComplete.call(this); + } + return this; + }, + generateLegend : function(){ + return template(this.options.legendTemplate,this); + }, + destroy : function(){ + this.clear(); + unbindEvents(this, this.events); + delete Chart.instances[this.id]; + }, + showTooltip : function(ChartElements, forceRedraw){ + // Only redraw the chart if we've actually changed what we're hovering on. + if (typeof this.activeElements === 'undefined') this.activeElements = []; + + var isChanged = (function(Elements){ + var changed = false; + + if (Elements.length !== this.activeElements.length){ + changed = true; + return changed; + } + + each(Elements, function(element, index){ + if (element !== this.activeElements[index]){ + changed = true; + } + }, this); + return changed; + }).call(this, ChartElements); + + if (!isChanged && !forceRedraw){ + return; + } + else{ + this.activeElements = ChartElements; + } + this.draw(); + if (ChartElements.length > 0){ + // If we have multiple datasets, show a MultiTooltip for all of the data points at that index + if (this.datasets && this.datasets.length > 1) { + var dataArray, + dataIndex; + + for (var i = this.datasets.length - 1; i >= 0; i--) { + dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; + dataIndex = indexOf(dataArray, ChartElements[0]); + if (dataIndex !== -1){ + break; + } + } + var tooltipLabels = [], + tooltipColors = [], + medianPosition = (function(index) { + + // Get all the points at that particular index + var Elements = [], + dataCollection, + xPositions = [], + yPositions = [], + xMax, + yMax, + xMin, + yMin; + helpers.each(this.datasets, function(dataset){ + dataCollection = dataset.points || dataset.bars || dataset.segments; + Elements.push(dataCollection[dataIndex]); + }); + + helpers.each(Elements, function(element) { + xPositions.push(element.x); + yPositions.push(element.y); + + + //Include any colour information about the element + tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); + tooltipColors.push({ + fill: element._saved.fillColor || element.fillColor, + stroke: element._saved.strokeColor || element.strokeColor + }); + + }, this); + + yMin = min(yPositions); + yMax = max(yPositions); + + xMin = min(xPositions); + xMax = max(xPositions); + + return { + x: (xMin > this.chart.width/2) ? xMin : xMax, + y: (yMin + yMax)/2 + }; + }).call(this, dataIndex); + + new Chart.MultiTooltip({ + x: medianPosition.x, + y: medianPosition.y, + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + xOffset: this.options.tooltipXOffset, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + titleTextColor: this.options.tooltipTitleFontColor, + titleFontFamily: this.options.tooltipTitleFontFamily, + titleFontStyle: this.options.tooltipTitleFontStyle, + titleFontSize: this.options.tooltipTitleFontSize, + cornerRadius: this.options.tooltipCornerRadius, + labels: tooltipLabels, + legendColors: tooltipColors, + legendColorBackground : this.options.multiTooltipKeyBackground, + title: ChartElements[0].label, + chart: this.chart, + ctx: this.chart.ctx + }).draw(); + + } else { + each(ChartElements, function(Element) { + var tooltipPosition = Element.tooltipPosition(); + new Chart.Tooltip({ + x: Math.round(tooltipPosition.x), + y: Math.round(tooltipPosition.y), + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + caretHeight: this.options.tooltipCaretSize, + cornerRadius: this.options.tooltipCornerRadius, + text: template(this.options.tooltipTemplate, Element), + chart: this.chart + }).draw(); + }, this); + } + } + return this; + }, + toBase64Image : function(){ + return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); + } + }); + + Chart.Type.extend = function(extensions){ + + var parent = this; + + var ChartType = function(){ + return parent.apply(this,arguments); + }; + + //Copy the prototype object of the this class + ChartType.prototype = clone(parent.prototype); + //Now overwrite some of the properties in the base class with the new extensions + extend(ChartType.prototype, extensions); + + ChartType.extend = Chart.Type.extend; + + if (extensions.name || parent.prototype.name){ + + var chartName = extensions.name || parent.prototype.name; + //Assign any potential default values of the new chart type + + //If none are defined, we'll use a clone of the chart type this is being extended from. + //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart + //doesn't define some defaults of their own. + + var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; + + Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); + + Chart.types[chartName] = ChartType; + + //Register this new chart type in the Chart prototype + Chart.prototype[chartName] = function(data,options){ + var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); + return new ChartType(data,config,this); + }; + } else{ + warn("Name not provided for this chart, so it hasn't been registered"); + } + return parent; + }; + + Chart.Element = function(configuration){ + extend(this,configuration); + this.initialize.apply(this,arguments); + this.save(); + }; + extend(Chart.Element.prototype,{ + initialize : function(){}, + restore : function(props){ + if (!props){ + extend(this,this._saved); + } else { + each(props,function(key){ + this[key] = this._saved[key]; + },this); + } + return this; + }, + save : function(){ + this._saved = clone(this); + delete this._saved._saved; + return this; + }, + update : function(newProps){ + each(newProps,function(value,key){ + this._saved[key] = this[key]; + this[key] = value; + },this); + return this; + }, + transition : function(props,ease){ + each(props,function(value,key){ + this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; + },this); + return this; + }, + tooltipPosition : function(){ + return { + x : this.x, + y : this.y + }; + } + }); + + Chart.Element.extend = inherits; + + + Chart.Point = Chart.Element.extend({ + inRange : function(chartX,chartY){ + var hitDetectionRange = this.hitDetectionRadius + this.radius; + return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); + }, + draw : function(){ + var ctx = this.ctx; + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); + ctx.closePath(); + + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.stroke(); + + + + //Quick debug for bezier curve splining + //Highlights control points and the line between them. + //Handy for dev - stripped in the min version. + + // ctx.save(); + // ctx.fillStyle = "black"; + // ctx.strokeStyle = "black" + // ctx.beginPath(); + // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.beginPath(); + // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); + // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); + // ctx.stroke(); + + // ctx.restore(); + + + + } + }); + + Chart.Arc = Chart.Element.extend({ + inRange : function(chartX,chartY){ + + var pointRelativePosition = helpers.getAngleFromPoint(this, { + x: chartX, + y: chartY + }); + + //Check if within the range of the open/close angle + var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), + withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); + + return (betweenAngles && withinRadius); + //Ensure within the outside of the arc centre, but inside arc outer + }, + tooltipPosition : function(){ + var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), + rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; + return { + x : this.x + (Math.cos(centreAngle) * rangeFromCentre), + y : this.y + (Math.sin(centreAngle) * rangeFromCentre) + }; + }, + draw : function(animationPercent){ + + var easingDecimal = animationPercent || 1; + + var ctx = this.ctx; + + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); + + ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); + + ctx.closePath(); + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.lineJoin = 'bevel'; + + if (this.showStroke){ + ctx.stroke(); + } + } + }); + + Chart.Rectangle = Chart.Element.extend({ + draw : function(){ + var ctx = this.ctx, + halfWidth = this.width/2, + leftX = this.x - halfWidth, + rightX = this.x + halfWidth, + top = this.base - (this.base - this.y), + halfStroke = this.strokeWidth / 2; + + // Canvas doesn't allow us to stroke inside the width so we can + // adjust the sizes to fit if we're setting a stroke on the line + if (this.showStroke){ + leftX += halfStroke; + rightX -= halfStroke; + top += halfStroke; + } + + ctx.beginPath(); + + ctx.fillStyle = this.fillColor; + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + // It'd be nice to keep this class totally generic to any rectangle + // and simply specify which border to miss out. + ctx.moveTo(leftX, this.base); + ctx.lineTo(leftX, top); + ctx.lineTo(rightX, top); + ctx.lineTo(rightX, this.base); + ctx.fill(); + if (this.showStroke){ + ctx.stroke(); + } + }, + height : function(){ + return this.base - this.y; + }, + inRange : function(chartX,chartY){ + return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); + } + }); + + Chart.Tooltip = Chart.Element.extend({ + draw : function(){ + + var ctx = this.chart.ctx; + + ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.xAlign = "center"; + this.yAlign = "above"; + + //Distance between the actual element.y position and the start of the tooltip caret + var caretPadding = 2; + + var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, + tooltipRectHeight = this.fontSize + 2*this.yPadding, + tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; + + if (this.x + tooltipWidth/2 >this.chart.width){ + this.xAlign = "left"; + } else if (this.x - tooltipWidth/2 < 0){ + this.xAlign = "right"; + } + + if (this.y - tooltipHeight < 0){ + this.yAlign = "below"; + } + + + var tooltipX = this.x - tooltipWidth/2, + tooltipY = this.y - tooltipHeight; + + ctx.fillStyle = this.fillColor; + + switch(this.yAlign) + { + case "above": + //Draw a caret above the x/y + ctx.beginPath(); + ctx.moveTo(this.x,this.y - caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.closePath(); + ctx.fill(); + break; + case "below": + tooltipY = this.y + caretPadding + this.caretHeight; + //Draw a caret below the x/y + ctx.beginPath(); + ctx.moveTo(this.x, this.y + caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.closePath(); + ctx.fill(); + break; + } + + switch(this.xAlign) + { + case "left": + tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); + break; + case "right": + tooltipX = this.x - (this.cornerRadius + this.caretHeight); + break; + } + + drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); + + ctx.fill(); + + ctx.fillStyle = this.textColor; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); + } + }); + + Chart.MultiTooltip = Chart.Element.extend({ + initialize : function(){ + this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); + + this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; + + this.ctx.font = this.titleFont; + + var titleWidth = this.ctx.measureText(this.title).width, + //Label has a legend square as well so account for this. + labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, + longestTextWidth = max([labelWidth,titleWidth]); + + this.width = longestTextWidth + (this.xPadding*2); + + + var halfHeight = this.height/2; + + //Check to ensure the height will fit on the canvas + //The three is to buffer form the very + if (this.y - halfHeight < 0 ){ + this.y = halfHeight; + } else if (this.y + halfHeight > this.chart.height){ + this.y = this.chart.height - halfHeight; + } + + //Decide whether to align left or right based on position on canvas + if (this.x > this.chart.width/2){ + this.x -= this.xOffset + this.width; + } else { + this.x += this.xOffset; + } + + + }, + getLineHeight : function(index){ + var baseLineHeight = this.y - (this.height/2) + this.yPadding, + afterTitleIndex = index-1; + + //If the index is zero, we're getting the title + if (index === 0){ + return baseLineHeight + this.titleFontSize/2; + } else{ + return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; + } + + }, + draw : function(){ + drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); + var ctx = this.ctx; + ctx.fillStyle = this.fillColor; + ctx.fill(); + ctx.closePath(); + + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.titleTextColor; + ctx.font = this.titleFont; + + ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); + + ctx.font = this.font; + helpers.each(this.labels,function(label,index){ + ctx.fillStyle = this.textColor; + ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); + + //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) + //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + //Instead we'll make a white filled block to put the legendColour palette over. + + ctx.fillStyle = this.legendColorBackground; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + ctx.fillStyle = this.legendColors[index].fill; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + + },this); + } + }); + + Chart.Scale = Chart.Element.extend({ + initialize : function(){ + this.fit(); + }, + buildYLabels : function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + 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; + }, + addXLabel : function(label){ + this.xLabels.push(label); + this.valuesCount++; + this.fit(); + }, + removeXLabel : function(){ + this.xLabels.shift(); + this.valuesCount--; + this.fit(); + }, + // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use + fit: function(){ + // First we need the width of the yLabels, assuming the xLabels aren't rotated + + // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation + this.startPoint = (this.display) ? this.fontSize : 0; + this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels + + // Apply padding settings to the start and end point. + this.startPoint += this.padding; + this.endPoint -= this.padding; + + // Cache the starting height, so can determine if we need to recalculate the scale yAxis + var cachedHeight = this.endPoint - this.startPoint, + cachedYLabelWidth; + + // Build the current yLabels so we have an idea of what size they'll be to start + /* + * This sets what is returned from calculateScaleRange as static properties of this class: + * + this.steps; + this.stepValue; + this.min; + this.max; + * + */ + this.calculateYRange(cachedHeight); + + // With these properties set we can now build the array of yLabels + // and also the width of the largest yLabel + this.buildYLabels(); + + this.calculateXLabelRotation(); + + while((cachedHeight > this.endPoint - this.startPoint)){ + cachedHeight = this.endPoint - this.startPoint; + cachedYLabelWidth = this.yLabelWidth; + + this.calculateYRange(cachedHeight); + this.buildYLabels(); + + // Only go through the xLabel loop again if the yLabel width has changed + if (cachedYLabelWidth < this.yLabelWidth){ + this.calculateXLabelRotation(); + } + } + + }, + calculateXLabelRotation : function(){ + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + + this.ctx.font = this.font; + + var firstWidth = this.ctx.measureText(this.xLabels[0]).width, + lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, + firstRotated, + lastRotated; + + + this.xScalePaddingRight = lastWidth/2 + 3; + this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; + + this.xLabelRotation = 0; + if (this.display){ + var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), + cosRotation, + firstRotatedWidth; + this.xLabelWidth = originalLabelWidth; + //Allow 3 pixels x2 padding either side for label readability + var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; + + //Max label rotate should be 90 - also act as a loop counter + while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ + cosRotation = Math.cos(toRadians(this.xLabelRotation)); + + firstRotated = cosRotation * firstWidth; + lastRotated = cosRotation * lastWidth; + + // We're right aligning the text now. + if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ + this.xScalePaddingLeft = firstRotated + this.fontSize / 2; + } + this.xScalePaddingRight = this.fontSize/2; + + + this.xLabelRotation++; + this.xLabelWidth = cosRotation * originalLabelWidth; + + } + if (this.xLabelRotation > 0){ + this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; + } + } + else{ + this.xLabelWidth = 0; + this.xScalePaddingRight = this.padding; + this.xScalePaddingLeft = this.padding; + } + + }, + // Needs to be overidden in each Chart type + // Otherwise we need to pass all the data into the scale class + calculateYRange: noop, + drawingArea: function(){ + return this.startPoint - this.endPoint; + }, + calculateY : function(value){ + var scalingFactor = this.drawingArea() / (this.min - this.max); + return this.endPoint - (scalingFactor * (value - this.min)); + }, + calculateX : function(index){ + var isRotated = (this.xLabelRotation > 0), + // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, + innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), + valueWidth = innerWidth/(this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), + valueOffset = (valueWidth * index) + this.xScalePaddingLeft; + + if (this.offsetGridLines){ + valueOffset += (valueWidth/2); + } + + return Math.round(valueOffset); + }, + update : function(newProps){ + helpers.extend(this, newProps); + this.fit(); + }, + draw : function(){ + var ctx = this.ctx, + yLabelGap = (this.endPoint - this.startPoint) / this.steps, + xStart = Math.round(this.xScalePaddingLeft); + if (this.display){ + ctx.fillStyle = this.textColor; + ctx.font = this.font; + each(this.yLabels,function(labelString,index){ + var yLabelCenter = this.endPoint - (yLabelGap * index), + linePositionY = Math.round(yLabelCenter); + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + if (this.showLabels){ + ctx.fillText(labelString,xStart - 10,yLabelCenter); + } + ctx.beginPath(); + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + linePositionY += helpers.aliasPixel(ctx.lineWidth); + + ctx.moveTo(xStart, linePositionY); + ctx.lineTo(this.width, linePositionY); + ctx.stroke(); + ctx.closePath(); + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + ctx.beginPath(); + ctx.moveTo(xStart - 5, linePositionY); + ctx.lineTo(xStart, linePositionY); + ctx.stroke(); + ctx.closePath(); + + },this); + + each(this.xLabels,function(label,index){ + var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), + // Check to see if line/bar here and decide where to place the line + linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), + isRotated = (this.xLabelRotation > 0); + + ctx.beginPath(); + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.startPoint - 3); + ctx.stroke(); + ctx.closePath(); + + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + + + // Small lines at the bottom of the base grid line + ctx.beginPath(); + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.endPoint + 5); + ctx.stroke(); + ctx.closePath(); + + ctx.save(); + ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); + ctx.rotate(toRadians(this.xLabelRotation)*-1); + ctx.font = this.font; + ctx.textAlign = (isRotated) ? "right" : "center"; + ctx.textBaseline = (isRotated) ? "middle" : "top"; + ctx.fillText(label, 0, 0); + ctx.restore(); + },this); + + } + } + + }); + + Chart.RadialScale = Chart.Element.extend({ + initialize: function(){ + this.size = min([this.height, this.width]); + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + }, + calculateCenterOffset: function(value){ + // Take into account half font size + the yPadding of the top value + var scalingFactor = this.drawingArea / (this.max - this.min); + + return (value - this.min) * scalingFactor; + }, + update : function(){ + if (!this.lineArc){ + this.setScaleSize(); + } else { + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + } + this.buildYLabels(); + }, + buildYLabels: function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + }, + getCircumference : function(){ + return ((Math.PI*2) / this.valuesCount); + }, + setScaleSize: function(){ + /* + * Right, this is really confusing and there is a lot of maths going on here + * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + * + * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + * + * Solution: + * + * We assume the radius of the polygon is half the size of the canvas at first + * at each index we check if the text overlaps. + * + * Where it does, we store that angle and that index. + * + * After finding the largest index and angle we calculate how much we need to remove + * from the shape radius to move the point inwards by that x. + * + * We average the left and right distances to get the maximum shape radius that can fit in the box + * along with labels. + * + * Once we have that, we can find the centre point for the chart, by taking the x text protrusion + * on each side, removing that from the size, halving it and adding the left x protrusion width. + * + * This will mean we have a shape fitted to the canvas, as large as it can be with the labels + * and position it in the most space efficient manner + * + * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + */ + + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), + pointPosition, + i, + textWidth, + halfTextWidth, + furthestRight = this.width, + furthestRightIndex, + furthestRightAngle, + furthestLeft = 0, + furthestLeftIndex, + furthestLeftAngle, + xProtrusionLeft, + xProtrusionRight, + radiusReductionRight, + radiusReductionLeft, + maxWidthRadius; + this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + for (i=0;i furthestRight) { + furthestRight = pointPosition.x + halfTextWidth; + furthestRightIndex = i; + } + if (pointPosition.x - halfTextWidth < furthestLeft) { + furthestLeft = pointPosition.x - halfTextWidth; + furthestLeftIndex = i; + } + } + else if (i < this.valuesCount/2) { + // Less than half the values means we'll left align the text + if (pointPosition.x + textWidth > furthestRight) { + furthestRight = pointPosition.x + textWidth; + furthestRightIndex = i; + } + } + else if (i > this.valuesCount/2){ + // More than half the values means we'll right align the text + if (pointPosition.x - textWidth < furthestLeft) { + furthestLeft = pointPosition.x - textWidth; + furthestLeftIndex = i; + } + } + } + + xProtrusionLeft = furthestLeft; + + xProtrusionRight = Math.ceil(furthestRight - this.width); + + furthestRightAngle = this.getIndexAngle(furthestRightIndex); + + furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); + + radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); + + radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); + + // Ensure we actually need to reduce the size of the chart + radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; + radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; + + this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; + + //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) + this.setCenterPoint(radiusReductionLeft, radiusReductionRight); + + }, + setCenterPoint: function(leftMovement, rightMovement){ + + var maxRight = this.width - rightMovement - this.drawingArea, + maxLeft = leftMovement + this.drawingArea; + + this.xCenter = (maxLeft + maxRight)/2; + // Always vertically in the centre as the text height doesn't change + this.yCenter = (this.height/2); + }, + + getIndexAngle : function(index){ + var angleMultiplier = (Math.PI * 2) / this.valuesCount; + // Start from the top instead of right, so remove a quarter of the circle + + return index * angleMultiplier - (Math.PI/2); + }, + getPointPosition : function(index, distanceFromCenter){ + var thisAngle = this.getIndexAngle(index); + return { + x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, + y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter + }; + }, + draw: function(){ + if (this.display){ + var ctx = this.ctx; + each(this.yLabels, function(label, index){ + // Don't draw a centre value + if (index > 0){ + var yCenterOffset = index * (this.drawingArea/this.steps), + yHeight = this.yCenter - yCenterOffset, + pointPosition; + + // Draw circular lines around the scale + if (this.lineWidth > 0){ + ctx.strokeStyle = this.lineColor; + ctx.lineWidth = this.lineWidth; + + if(this.lineArc){ + ctx.beginPath(); + ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); + ctx.closePath(); + ctx.stroke(); + } else{ + ctx.beginPath(); + for (var i=0;i= 0; i--) { + if (this.angleLineWidth > 0){ + var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); + ctx.beginPath(); + ctx.moveTo(this.xCenter, this.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); + ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + ctx.fillStyle = this.pointLabelFontColor; + + var labelsCount = this.labels.length, + halfLabelsCount = this.labels.length/2, + quarterLabelsCount = halfLabelsCount/2, + upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), + exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); + if (i === 0){ + ctx.textAlign = 'center'; + } else if(i === halfLabelsCount){ + ctx.textAlign = 'center'; + } else if (i < halfLabelsCount){ + ctx.textAlign = 'left'; + } else { + ctx.textAlign = 'right'; + } + + // Set the correct text baseline based on outer positioning + if (exactQuarter){ + ctx.textBaseline = 'middle'; + } else if (upperHalf){ + ctx.textBaseline = 'bottom'; + } else { + ctx.textBaseline = 'top'; + } + + ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); + } + } + } + } + }); + + // 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. + var timeout; + return function(){ + clearTimeout(timeout); + timeout = setTimeout(function(){ + each(Chart.instances,function(instance){ + // If the responsive flag is set in the chart instance config + // Cascade the resize event down to the chart. + if (instance.options.responsive){ + instance.resize(instance.render, true); + } + }); + }, 50); + }; + })()); + + + if (amd) { + define(function(){ + return Chart; + }); + } + + root.Chart = Chart; + + Chart.noConflict = function(){ + root.Chart = previous; + return Chart; + }; + +}).call(this); \ No newline at end of file diff --git a/src/Chart.Doughnut.js b/src/Chart.Doughnut.js new file mode 100644 index 00000000000..35de93c7fd6 --- /dev/null +++ b/src/Chart.Doughnut.js @@ -0,0 +1,184 @@ +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Whether we should show a stroke on each segment + segmentShowStroke : true, + + //String - The colour of each segment stroke + segmentStrokeColor : "#fff", + + //Number - The width of each segment stroke + segmentStrokeWidth : 2, + + //The percentage of the chart that we cut out of the middle. + percentageInnerCutout : 50, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect + animationEasing : "easeOutBounce", + + //Boolean - Whether we animate the rotation of the Doughnut + animateRotate : true, + + //Boolean - Whether we animate scaling the Doughnut from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    " + + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "Doughnut", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + + //Declare segments as a static property to prevent inheriting across the Chart type prototype + this.segments = []; + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + + this.SegmentArc = Chart.Arc.extend({ + ctx : this.chart.ctx, + x : this.chart.width/2, + y : this.chart.height/2 + }); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + this.calculateTotal(data); + + helpers.each(data,function(datapoint, index){ + this.addData(datapoint, index, true); + },this); + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + this.segments.splice(index, 0, new this.SegmentArc({ + value : segment.value, + outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, + innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, + fillColor : segment.color, + highlightColor : segment.highlight || segment.color, + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + startAngle : Math.PI * 1.5, + circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), + label : segment.label + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + calculateCircumference : function(value){ + return (Math.PI*2)*(value / this.total); + }, + calculateTotal : function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += segment.value; + },this); + }, + update : function(){ + this.calculateTotal(this.segments); + + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor']); + }); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + this.render(); + }, + + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + }); + }, this); + }, + draw : function(easeDecimal){ + var animDecimal = (easeDecimal) ? easeDecimal : 1; + this.clear(); + helpers.each(this.segments,function(segment,index){ + segment.transition({ + circumference : this.calculateCircumference(segment.value), + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + },animDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + segment.draw(); + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length-1){ + this.segments[index+1].startAngle = segment.endAngle; + } + },this); + + } + }); + + Chart.types.Doughnut.extend({ + name : "Pie", + defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) + }); + +}).call(this); \ No newline at end of file diff --git a/src/Chart.Line.js b/src/Chart.Line.js new file mode 100644 index 00000000000..42e546c72f9 --- /dev/null +++ b/src/Chart.Line.js @@ -0,0 +1,343 @@ +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + + ///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 the line is curved between points + bezierCurve : true, + + //Number - Tension of the bezier curve between points + bezierCurveTension : 0.4, + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 4, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " + + }; + + + Chart.Type.extend({ + name: "Line", + defaults : defaultConfig, + 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({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx, + inRange : function(mouseX){ + return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePoints, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + this.showTooltip(activePoints); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + + helpers.each(dataset.data,function(dataPoint,index){ + //Best way to do this? or in draw sequence...? + if (helpers.isNumber(dataPoint)){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + // x: this.scale.calculateX(index), + // y: this.scale.endPoint, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + } + },this); + + this.buildScale(data.labels); + + + this.eachPoints(function(point, index){ + helpers.extend(point, { + x: this.scale.calculateX(index), + y: this.scale.endPoint + }); + point.save(); + }, this); + + },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.eachPoints(function(point){ + point.save(); + }); + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + getPointsAtEvent : function(e){ + var pointsArray = [], + eventPosition = helpers.getRelativePosition(e); + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,function(point){ + if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); + }); + },this); + return pointsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachPoints(function(point){ + values.push(point.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, + 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.pointDotRadius + this.options.pointDotStrokeWidth, + 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 Chart.Scale(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + + helpers.each(valuesArray,function(value,datasetIndex){ + if (helpers.isNumber(value)){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: this.scale.calculateX(this.scale.valuesCount+1), + y: this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + } + },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.points.shift(); + },this); + this.update(); + }, + reflow : function(){ + 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); + + + helpers.each(this.datasets,function(dataset){ + + //Transition each point first so that the line and point drawing isn't out of sync + //We can use this extra loop to calculate the control points of this dataset also in this loop + + helpers.each(dataset.points,function(point,index){ + point.transition({ + y : this.scale.calculateY(point.value), + x : this.scale.calculateX(index) + }, easingDecimal); + + },this); + + + // Control points need to be calculated in a seperate 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(dataset.points,function(point,index){ + //If we're at the start or end, we don't have a previous/next point + //By setting the tension to 0 here, the curve will transition to straight at the end + if (index === 0){ + point.controlPoints = helpers.splineCurve(point,point,dataset.points[index+1],0); + } + else if (index >= dataset.points.length-1){ + point.controlPoints = helpers.splineCurve(dataset.points[index-1],point,point,0); + } + else{ + point.controlPoints = helpers.splineCurve(dataset.points[index-1],point,dataset.points[index+1],this.options.bezierCurveTension); + } + },this); + } + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + helpers.each(dataset.points,function(point,index){ + if (index>0){ + if(this.options.bezierCurve){ + ctx.bezierCurveTo( + dataset.points[index-1].controlPoints.outer.x, + dataset.points[index-1].controlPoints.outer.y, + point.controlPoints.inner.x, + point.controlPoints.inner.y, + point.x, + point.y + ); + } + else{ + ctx.lineTo(point.x,point.y); + } + + } + else{ + ctx.moveTo(point.x,point.y); + } + },this); + ctx.stroke(); + + + if (this.options.datasetFill){ + //Round off the line by going to the base of the chart, back to the start, then fill. + ctx.lineTo(dataset.points[dataset.points.length-1].x, this.scale.endPoint); + ctx.lineTo(this.scale.calculateX(0), this.scale.endPoint); + ctx.fillStyle = dataset.fillColor; + ctx.closePath(); + ctx.fill(); + } + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(dataset.points,function(point){ + point.draw(); + }); + + },this); + } + }); + + +}).call(this); \ No newline at end of file diff --git a/src/Chart.PolarArea.js b/src/Chart.PolarArea.js new file mode 100644 index 00000000000..4dbf2eb92a0 --- /dev/null +++ b/src/Chart.PolarArea.js @@ -0,0 +1,248 @@ +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Show a backdrop to the scale label + scaleShowLabelBackdrop : true, + + //String - The colour of the label backdrop + scaleBackdropColor : "rgba(255,255,255,0.75)", + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //Number - The backdrop padding above & below the label in pixels + scaleBackdropPaddingY : 2, + + //Number - The backdrop padding to the side of the label in pixels + scaleBackdropPaddingX : 2, + + //Boolean - Show line for each value in the scale + scaleShowLine : true, + + //Boolean - Stroke a line around each segment in the chart + segmentShowStroke : true, + + //String - The colour of the stroke on each segement. + segmentStrokeColor : "#fff", + + //Number - The width of the stroke value in pixels + segmentStrokeWidth : 2, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect. + animationEasing : "easeOutBounce", + + //Boolean - Whether to animate the rotation of the chart + animateRotate : true, + + //Boolean - Whether to animate scaling the chart from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    " + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "PolarArea", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + this.segments = []; + //Declare segment class as a chart instance specific class, so it can share props for this instance + this.SegmentArc = Chart.Arc.extend({ + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + ctx : this.chart.ctx, + innerRadius : 0, + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + lineArc: true, + width: this.chart.width, + height: this.chart.height, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + valuesCount: data.length + }); + + this.updateScaleRange(data); + + this.scale.update(); + + helpers.each(data,function(segment,index){ + this.addData(segment,index,true); + },this); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + + this.segments.splice(index, 0, new this.SegmentArc({ + fillColor: segment.color, + highlightColor: segment.highlight || segment.color, + label: segment.label, + value: segment.value, + outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), + circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), + startAngle: Math.PI * 1.5 + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + calculateTotal: function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += segment.value; + },this); + this.scale.valuesCount = this.segments.length; + }, + updateScaleRange: function(datapoints){ + var valuesArray = []; + helpers.each(datapoints,function(segment){ + valuesArray.push(segment.value); + }); + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes, + { + size: helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + } + ); + + }, + update : function(){ + this.calculateTotal(this.segments); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + this.render(); + }, + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.updateScaleRange(this.segments); + this.scale.update(); + + helpers.extend(this.scale,{ + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.scale.calculateCenterOffset(segment.value) + }); + }, this); + + }, + draw : function(ease){ + var easingDecimal = ease || 1; + //Clear & draw the canvas + this.clear(); + helpers.each(this.segments,function(segment, index){ + segment.transition({ + circumference : this.scale.getCircumference(), + outerRadius : this.scale.calculateCenterOffset(segment.value) + },easingDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + // If we've removed the first segment we need to set the first one to + // start at the top. + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length - 1){ + this.segments[index+1].startAngle = segment.endAngle; + } + segment.draw(); + }, this); + this.scale.draw(); + } + }); + +}).call(this); \ No newline at end of file diff --git a/src/Chart.Radar.js b/src/Chart.Radar.js new file mode 100644 index 00000000000..66eccaba364 --- /dev/null +++ b/src/Chart.Radar.js @@ -0,0 +1,341 @@ +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + + Chart.Type.extend({ + name: "Radar", + defaults:{ + //Boolean - Whether to show lines for each scale point + scaleShowLine : true, + + //Boolean - Whether we show the angle lines out of the radar + angleShowLineOut : true, + + //Boolean - Whether to show labels on the scale + scaleShowLabels : false, + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //String - Colour of the angle line + angleLineColor : "rgba(0,0,0,.1)", + + //Number - Pixel width of the angle line + angleLineWidth : 1, + + //String - Point label font declaration + pointLabelFontFamily : "'Arial'", + + //String - Point label font weight + pointLabelFontStyle : "normal", + + //Number - Point label font size in pixels + pointLabelFontSize : 10, + + //String - Point label font colour + pointLabelFontColor : "#666", + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 3, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " + + }, + + initialize: function(data){ + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx + }); + + this.datasets = []; + + this.buildScale(data); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePointsCollection, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + + this.showTooltip(activePointsCollection); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label: dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Best way to do this? or in draw sequence...? + if (helpers.isNumber(dataPoint)){ + //Add a new point for each piece of data, passing any required data to draw. + var pointPosition; + if (!this.scale.animation){ + pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); + } + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, + y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + } + },this); + + },this); + + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + + getPointsAtEvent : function(evt){ + var mousePosition = helpers.getRelativePosition(evt), + fromCenter = helpers.getAngleFromPoint({ + x: this.scale.xCenter, + y: this.scale.yCenter + }, mousePosition); + + var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, + pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), + activePointsCollection = []; + + // If we're at the top, make the pointIndex 0 to get the first of the array. + if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ + pointIndex = 0; + } + + if (fromCenter.distance <= this.scale.drawingArea){ + helpers.each(this.datasets, function(dataset){ + activePointsCollection.push(dataset.points[pointIndex]); + }); + } + + return activePointsCollection; + }, + + buildScale : function(data){ + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + angleLineColor : this.options.angleLineColor, + angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, + // Point labels at the edge of each line + pointLabelFontColor : this.options.pointLabelFontColor, + pointLabelFontSize : this.options.pointLabelFontSize, + pointLabelFontFamily : this.options.pointLabelFontFamily, + pointLabelFontStyle : this.options.pointLabelFontStyle, + height : this.chart.height, + width: this.chart.width, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + labels: data.labels, + valuesCount: data.datasets[0].data.length + }); + + this.scale.setScaleSize(); + this.updateScaleRange(data.datasets); + this.scale.buildYLabels(); + }, + updateScaleRange: function(datasets){ + var valuesArray = (function(){ + var totalDataArray = []; + helpers.each(datasets,function(dataset){ + if (dataset.data){ + totalDataArray = totalDataArray.concat(dataset.data); + } + else { + helpers.each(dataset.points, function(point){ + totalDataArray.push(point.value); + }); + } + }); + return totalDataArray; + })(); + + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes + ); + + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + this.scale.valuesCount++; + helpers.each(valuesArray,function(value,datasetIndex){ + if (helpers.isNumber(value)){ + var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: pointPosition.x, + y: pointPosition.y, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + } + },this); + + this.scale.labels.push(label); + + this.reflow(); + + this.update(); + }, + removeData : function(){ + this.scale.valuesCount--; + this.scale.labels.shift(); + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.reflow(); + this.update(); + }, + update : function(){ + this.eachPoints(function(point){ + point.save(); + }); + this.render(); + }, + reflow: function(){ + helpers.extend(this.scale, { + width : this.chart.width, + height: this.chart.height, + size : helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + this.updateScaleRange(this.datasets); + this.scale.setScaleSize(); + this.scale.buildYLabels(); + }, + draw : function(ease){ + var easeDecimal = ease || 1, + ctx = this.chart.ctx; + this.clear(); + this.scale.draw(); + + helpers.each(this.datasets,function(dataset){ + + //Transition each point first so that the line and point drawing isn't out of sync + helpers.each(dataset.points,function(point,index){ + point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); + },this); + + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + helpers.each(dataset.points,function(point,index){ + if (index === 0){ + ctx.moveTo(point.x,point.y); + } + else{ + ctx.lineTo(point.x,point.y); + } + },this); + ctx.closePath(); + ctx.stroke(); + + ctx.fillStyle = dataset.fillColor; + ctx.fill(); + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(dataset.points,function(point){ + point.draw(); + }); + + },this); + + } + + }); + + + + + +}).call(this); \ No newline at end of file From e24efd23a2bbd9db6197998f9b8f1ab33d1df6f2 Mon Sep 17 00:00:00 2001 From: Nick Downie Date: Sun, 29 Jun 2014 18:37:07 +0100 Subject: [PATCH 7/9] Add build sequence + dev dependencies --- gulpfile.js | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 21 +++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 gulpfile.js create mode 100644 package.json diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000000..7ffa807581c --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,87 @@ +var gulp = require('gulp'), + concat = require('gulp-concat'), + uglify = require('gulp-uglify'), + util = require('gulp-util'), + jshint = require('gulp-jshint'), + size = require('gulp-size'), + connect = require('gulp-connect'), + exec = require('child_process').exec; + +var srcDir = './src/'; +/* + * Usage : gulp build --types=Bar,Line,Doughnut + * Output: - A built Chart.js file with Core and types Bar, Line and Doughnut concatenated together + * - A minified version of this code, in Chart.min.js + */ + +gulp.task('build', function(){ + + // Default to all of the chart types, with Chart.Core first + var srcFiles = [FileName('Core')], + isCustom = !!(util.env.types), + outputDir = (isCustom) ? 'custom' : '.'; + if (isCustom){ + util.env.types.split(',').forEach(function(type){ return srcFiles.push(FileName(type))}); + } + else{ + // Seems gulp-concat remove duplicates - nice! + // So we can use this to sort out dependency order - aka include Core first! + srcFiles.push(srcDir+'*'); + } + return gulp.src(srcFiles) + .pipe(concat('Chart.js')) + .pipe(gulp.dest(outputDir)) + .pipe(uglify({preserveComments:'some'})) + .pipe(concat('Chart.min.js')) + .pipe(gulp.dest(outputDir)); + + function FileName(moduleName){ + return srcDir+'Chart.'+moduleName+'.js'; + }; +}); + +gulp.task('jshint', function(){ + return gulp.src(srcDir + '*.js') + .pipe(jshint()) + .pipe(jshint.reporter('default')); +}); + +gulp.task('library-size', function(){ + return gulp.src('Chart.min.js') + .pipe(size({ + gzip: true + })); +}); + +gulp.task('module-sizes', function(){ + return gulp.src(srcDir + '*.js') + .pipe(uglify({preserveComments:'some'})) + .pipe(size({ + showFiles: true, + gzip: true + })) +}); + +gulp.task('watch', function(){ + gulp.watch('./src/*', ['build']); +}); + +gulp.task('test', ['jshint']); + +gulp.task('size', ['library-size', 'module-sizes']); + +gulp.task('default', ['build', 'watch']); + +gulp.task('server', function(){ + connect.server({ + port: 8000, + }); +}); + +// Convenience task for opening the project straight from the command line +gulp.task('_open', function(){ + exec('open http://localhost:8000'); + exec('subl .'); +}); + +gulp.task('dev', ['server', 'default']); diff --git a/package.json b/package.json new file mode 100644 index 00000000000..5e4d7250b06 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "Chart.js", + "homepage": "http://www.chartjs.org", + "description": "Simple HTML5 charts using the canvas element.", + "private": true, + "version": "1.0.0-beta", + "repository": { + "type": "git", + "url": "https://github.com/nnnick/Chart.js.git" + }, + "dependences": {}, + "devDependencies": { + "gulp": "3.5.x", + "gulp-concat": "~2.1.x", + "gulp-uglify": "~0.2.x", + "gulp-util": "~2.2.x", + "gulp-jshint": "~1.5.1", + "gulp-size": "~0.4.0", + "gulp-connect": "~2.0.5" + } +} From 0e5f4ec32f5dc98f3da8dff9792f19b425ce1c38 Mon Sep 17 00:00:00 2001 From: Nick Downie Date: Sun, 29 Jun 2014 18:37:29 +0100 Subject: [PATCH 8/9] Bump bower version --- bower.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bower.json b/bower.json index 11e11c79072..d83ec5300b8 100644 --- a/bower.json +++ b/bower.json @@ -1,8 +1,7 @@ { "name": "Chart.js", - "version": "0.2.0", + "version": "1.0.0-beta", "description": "Simple HTML5 Charts using the canvas element", - "keywords": ["charts"], "homepage": "https://github.com/nnnick/Chart.js", "author": "nnnick", "main": ["Chart.min.js"], From 8533c27031630bc02e2740f1d722fdbdd2bac32e Mon Sep 17 00:00:00 2001 From: Nick Downie Date: Sun, 29 Jun 2014 18:37:39 +0100 Subject: [PATCH 9/9] Update readme --- readme.md | 57 +++++++++++++++---------------------------------------- 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/readme.md b/readme.md index 228dc9cfc75..86d7c4c09db 100644 --- a/readme.md +++ b/readme.md @@ -1,52 +1,25 @@ -Chart.js -======= -*Simple HTML5 Charts using the canvas element* [chartjs.org](http://www.chartjs.org) - -Quick FYI -------- -I'm currently working on a big refactor of the library into a more object oriented structure. - -It'll have an extendable class structure for adding community developed chart types. By opening up components into Chart.js into extendable classes, it'll allow for much easier community driven library extensions rather than tacking on new features as required. The refactor will also feature a generisized version of the interaction layer introduced by Regaddi in his tooltips branch - https://github.com/nnnick/Chart.js/pull/51. On top of this, it'll include utility methods on each chart object, for updating, clearing and redrawing charts etc. - -I haven't quite got the bandwidth right now to be juggling both issues/requests in master while redesigning all of the core code in Chart.js. By focusing on the refactor, it'll get done WAY quicker. - -Extensibility will absolutely be at the core of the refactor, allowing for the development of complex extension modules, but also keeping a lightweight set of core code. - -Hang tight - it'll be worth it. - -PS. If you're interested in reviewing some code or trying out writing extensions, shoot me an email. +# Chart.js -###Update - 8th September -Just a quick update on the refactor. - -Just wanted to let you guys know it's making really good progress, and it'll be well worth the wait. - -The new version is being broken up into Chart type modules, with each of the current 6 chart types using documented and extendable classes and helper methods from the Chart.js core. This means the community will be able to build new chart types using existing components, or extend existing types to do something a bit different. - -By splitting the different charts into modules will mean the ability to use AMD if appropriate, but I'll also be writing a simple web interface for concatenating chart types into a minified production ready custom build. - -The syntax for creating charts **will not change**, so the upgrade should be a drop in replacement, but give you the ability to have a whole new level of interactivity and animated data updates. - -Right now I've wrote 80% of the core, and refactored the Doughnut and Pie charts, and I'm a good way through the Line and Bar charts. I hope to have the new version ready to release with some new docs late September/early October. +*Simple HTML5 Charts using the canvas element* [chartjs.org](http://www.chartjs.org) -I know PR and issues are racking up in the repo, and I'll do my best to sort them ASAP, but I think this update is really important for creating flexibility and extensibility to cater for these new features in an elegant way, rather than introducing scope creep into an architecture that wasn't designed to deliver this extra functionality. +## Documentation -Big thanks for all the support - it's been totally overwhelming. +You can find documentation at [chartjs.org/docs](http://www.chartjs.org/docs). The markdown files that build the site are available under `/docs`. Please note - in some of the json examples of configuration you might notice some liquid tags - this is just for the generating the site html, please disregard. -###Another Quick Update - 16th October -First of all - my apologies, early October has drifted away from me and we're moving towards late October. This last month has been really unexpectedly busy, and I've had a lot of stuff going on, so I haven't quite managed to find as much time to work on Chart.js as I'd hoped. +## License -In terms of an updated ETA, I'm really aiming for a pre-November release, and I'll be having some late nights and a few days off to try my best to make this happen. +Chart.js is available under the [MIT license](http://opensource.org/licenses/MIT). -Again, really appreciate the support and cheers for your patience for the new version. +## Bugs & issues +When reporting bugs or issues, if you could include a link to a simple [jsbin](http://jsbin.com) or similar demonstrating the issue, that'd be really helpful. -Documentation -------- -You can find documentation at [chartjs.org/docs](http://www.chartjs.org/docs). -License -------- -Chart.js was taken down on the 19th March. It is now back online for good and IS available under MIT license. +## Contributing +New contributions to the library are welcome, just a couple of guidelines: -Chart.js is available under the [MIT license] (http://opensource.org/licenses/MIT). +- Tabs for indentation, not spaces please. +- Please ensure you're changing the individual files in `/src`, not the concatenated output in the `Chart.js` file in the root of the repo. +- Please check that your code will pass `jshint` code standards, `gulp jshint` will run this for you. +- Please keep pull requests concise, and document new functionality in the relevant `.md` file. +- Consider whether your changes are useful for all users, or if creating a Chart.js extension would be more appropriate. \ No newline at end of file

    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 diff --git a/site/assets/simple.png b/site/assets/simple.png deleted file mode 100644 index 43c945d538aa8cefe7e5710235eea25a6c9fa858..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48416 zcmaI6b95%byDl2rnK+Ytv2ELSCbn(cwl#4kww+9D+r}3gcYgbvz0dvQ+}o?WS5@`< zJTF&uud0qzl$St+!-WF@0YQ|K6jcTR0e$$-1q1c(-Zclr|MvjLMNHF0#oo-t-O$Mt zM99S6$P^%DYiMq&Y-(uY={RP}3jzX8YN@K}qA4fKZESB#Z}=Y>dJkKNe`pX8UI7mW zLt|@G7l4texuqQ+F|f0j7+`6_N36jn$0+9@VrpS2>E&dq;w7(Y>}74tWkM{#58(CS z{zqVI>S74+u(h#s=Jwzt{x4qcf9?MaGY|v*OT@*RkNAHXr75Qf5V3bM1+dYx&>1r_ zF#*`w>6uvB*x8wA0nCg{%nXeGPIfvbR&F+SZe|X^|2~NSX>&3$<5m_G|KGa)_4tS_ zTwENu85rE%-Ra#~=SoaXX*cP!Pen_XziT;cbNVKjKRaufq{vh@jov8x1gNd|G%iM?f*zS zyC|FfAAA3=#LlXo4yFvsrq1@RPR9QdXGZcLRSw)DPNs$~_D-ty_BQ_=MMVpH7kg(5 zdk27s3Oj&O&d}J>?mq)m|Aml~b}_UwHkA_PBmT!hZ)s`5%_Ynv%*7?l$sxib z!o(!TEG#B2%qb+s%q%P}%qq?#{@++pdt+ByQ#+Uc#+v*eEZ6@h_CLj7>+nyrsHu~s zo2iMolf5nAzntc_{J+>D%*gyNPvYYL_=NuNw)`)w$^XTc|H29}GX8&J8U7i=@SoKF zzf%3*L;vdMKh6Is+<%?_DScDBfA#M4udoA)Jpw>L+LNS2g;YJ(54C+|e@OiB@AMw= z4bVY;zm=%8jrc1{^rMM9O$*SkzJU7%ji?0&HoP7cOV?a6SKMOxG9qbhEu5_=B_t+O z7r~wuvM65f-=sdojzTLPD4vx>t+(42-`#n2>9~5Zx|O|_y+!jxmb?+4nfvSwobtBc zzGeyn2y({8#|OtX;WzVllQ(W$HBSGEAdFN7B7U-u1cS267lcMwXOq=zsqB_9g1~vf z^%o4{!wnywG7*C&!NCWI=l!AHb#^NDLL5fJz{WVUMYQrhw9Ar(t*dxI z>>5jDWe}SFOO8ToHY|};+zOSoPVeoCB?^~J?wIZu6x(8AG2x*T*$cavWXa7dMq$jJ zZ8)*q$_7ag<0+F4k?FB#g-klmPy$J$x2D-I@mSUS^dzDrm3nNsZz2QV6dSw6r@!Kh zv;B6JHIL%Yx_#7lvLBKxlFu;^82p$8Zk{t;&_Z(No(|8hbEa#!Y0SK-4a}WcE!u%N3F{qIUY%(Fs;kcOa0fWVy@A`DS9D9?(NDNoT=;PMwkD z{OdPNu7Q5(%}M3o?b@xU0Svy7{@pBDnAx`#Pw}YSyTe&{^lE9g^9YIuQLG=rC}Ei8 z;T_Fum24X;6nMuC7h=w2ziA!b3y13r_XZ4tNc7M-Sz!3n3yVsT^d^GBV2b$o5NQV~ z`dHVwd_&p7+&B!k{m2cqRcdRGn@ivQeE6CS&>v)+uEh$G@=Vvh8D46&W-R_z&CS&c zc*L(nfA-n;@Ri}b6!&O-_V;Rku6JqA_}np5?}BII&_s}{cLFtt-;qro1IC)RRQoFL z;PJ&qD6p#{;0NH6b{G6*Xr8R!_DMMB-9TXjdKnZ=AHCguu5-Rb7?_`E4WO9(a)z>p9CIgQhI$! z?aRi&z6iL8>ay9$JI<6d`-s$z_+_sC86Z*3I$?icPUsY8TY`wC%i-|FQ3?S+8$|;C zry(&HyBH@;^w;J}Ij0}32+|zGwd`5)dFkEXcPteK7i(p354lU>L2AE}Tz@7W4j~Z8 zz!*t;A6~TUtErYO62X$81-eyjA_qcb@>WLcg`OD7Pt9f5?-!Wj;Wq!Ub0JV9@%adX5t|BE!JJlp zaqovn7|e=V+BYI!U($wZde@hQyfU|b{3~vvJVhAIy3&%Cm4DPIEZg~2lvX^^$xV|e zFsr!WWu+I38Giw(!!RWSEA#>GXwTd9li?qyKDH(~L7&*01c+S?J$n>2T>k5tt!}m| zE*d(7ks>M4Df|$`ZCO}`xDvcpC8bkffj$LUF{Q*MEi$xR+@?<8MK;PkfrG}m*U^Yv?uvN@cm z4KYpMUX)VN8KnJnbY*4teiD8rJO(FNr!e;YwD9e1U6!iJTbpd`H|m^ENmeiYT$PxB zOy&$fy^74sX819l1M{w^Fto15-gET9Y9h?}5Cob{eVfGV+X|z?>7K~4dGt9Q@{^$%Qy#-Ay7DyI2|sd@_YQjec)7StIfwxkRE3u8 z=GQ}uDJv0{ao;Meajtc~e&BgKc`aCUnL$71=qPf-!nUw{r9?@;%M|;4g^JQlgT*}$ zH`>d%Aoc;%pI}&=mflHojHl$1h^{2>Xni}d2ymQZ3j1KXUZo3IfJA2Mu&7f#7TT7oUZJ30t{}FW2x+noqX- zJDE>8Ae`w{vadZxY#~9#`fxcT=(-cO$&eu`+*_2KgFw-$0r8!f+j?SSbsnsiUKeTt z7cW;=a1KCNmktf~?V2KAGL#)|GCsSGn9${hscej+c%NRiron*Krt=xFkYFqk)yfa^ zilMs~7$2txP^ePQOojVMOWrELJvyKKv{#)q|6BC>619<@Oi_B4rMhe7nU&XMF=o*R z)N73TFpfn;bjsB!+R0YZna4!*NP%w~g7+Pvr0e0py)jC}N;plL%IyM&Xi9-YHfV7x zD*^fCQb@B6@|T@a+*L#k!&)Fc;r)ffSx z@EzC<8P7i_RO$$o{Q`dp{BYIIZX+#Ggz52tUzlT1$PG7{kBmkl{HDjXpfLcY8~#Y6 znqE|jd-6Sf6)3hf-utaSp(!`qdu!}5%mhS9zcg6^7G3e@gd0h5re<}HUV3>`#N^-v zPnvH(0}1%AyArN`Rx92W*;K$vO#GgCM{M}}>&P?(Pi9h;-mBWiS{rieWV`A3R|6N# zB@a1ov)oBh<8!V83*qXR0j`zv&pF-{I6<5)XZLd8-q}xSL6a1j#S)FO(TJ1^``~{UJ>RPqh8OBCF9s=~U zHK1`AxoiDXyBrDSRMJ(QCrBXI%L3W~m!H0>mBm=H!ad(7e+Yr(L1nq(#=0to-*c#) z%qmQ-%&TL|L~u;4*Q(abw~>)!;dcrYrB*66nLWMzZ#hP$UP+pga3_$WI=dk`E29OP z3IX`2($mRv;Ekj)uM^94HnGu*vz<2rWoie9i5Bi9+gfol2OQ&SFF>kV7lE4Azgv1u zIIUq9C5{YJd12gh<=>bA-86xb=P-lc8@dJz5UAGm)M;LU9!j>!QS0nhWOJfIGTncu z*sUaENez;hwXCgG4wj6%&I1dVA^<@iO>EV}%jFnpM$s(=Y*t#(Lz z&)u2sRX$_zsE^$o_fy|9-!s{-et$PKl0f*>>E0rWU}p^EsEc$hc&X1fmiOSFd!ykV zrWGL_HroEiUwd_PHp9R+#le!we9uO+ISbW|@Ui_&qt~}ShHI3N+-@G6KQrcrv_6iK zjCVKWrq%6Pp2WCjPSxDAG6*kuk3&UkFsq43cEVE)G@H_jg_{1f3UI}93s|!;WTVwW zzv}CqSnI$QuBR8X-e|^0>gvbJNz^SMy6~+-_D2Oq5zq1K(DR;IlJbp{u-)O|#^NUJ zWbIs6FzI#9-EtAK24E-Suuf>tZHR-1T3Q6WC84QL4k63lU_;0PlI@P}d1(up9+KG# zTvU>7o>^&s?HodwOTwJ0DCfQC9`INi$Yu2{!TW^LpC6rD(hpkkD&v16ApV+vXpb&O zy;$nL_Xopc1~PGAU00?sj?8|gjUsZ? zG-8=m&nj$%uy+Y2(a->-xs}jqX}>U$2w#!FU?0WA7;C%muUD)5>chmUQ$|UCAP@|k z+>U%kLPF%SI!?!kF@VESrjMM9qfh67#s+P0>F~}N-)RhWfdq?)fK!-ev(x0h1VYvC zEnfVl`Uc3dV{@Wk$C+x>)`2QBxu-KA&fi)tSFm6~;qR4R+SIvvLn$Lb6+WxA>F~Qf zOH1$5_8gz(96yXlryJNlxy?oNw{tGL^`A&MRy!xJc@GnoX(5hJq!UI-RW4ly$wUWK zw_!KXs|&L|oBqmYB=2G|5tVQrTfkA0+{w&_7RcqNpd3?Afd{U4Km}1rQnExC=vq-@ zD}C|xbRjfJ8@jOOAtHs*)jQ5pbU&Qj*BL*9_K!~QwBpE;HyX^qi-q*6y9Mg9{Q!&H z+e^UN((WiWrpe|vM}3T|V`UZG&WqiLayrclsL~?|SOP{99yYq8=C}|I<@S#C!-23T zz&V_d;*1Mb7j*LdHaTU?U%u^5lz*UGz+xy`KP3u=s(Zs!CjOX{9*s1Ftt+(`_%nlZ zVp^hblR}3Li7%cUsTIuO_eyo@s+GX}R`BoxJ~>W=J|9Y05g|~wYi(M=B{8GK|6`5Y zDmOEgN&hZ{;{+IyAo2rC<1ZSv;*eyoOM93!$!%KMOPe2nVpqO|t(S(>33+xG5>XoU za!!OtD2@`BJLdGWE(_l60y7+3*aL}@*!k^hokx{>I&PxkwP1omOcO;2`I0?i^ICReo|gBc*Uv+rZm4NB)dd>*>8&o*HOqo_CCNuBSL3^}F1j<| z8Fwyb>9nTg;gJJX!60-?1C4X%y#n8v)~t7LE-&sd+J0WtnU9xQb~Giaqktg>r?3wX z2FbV^RnZ*88O~;Jb*)YwET6Y%{dj+mrt5jJhF}GXyndh~H{j#ar$286n+<8Qd*{Iy z5Y@~yZ^10`wjb~F^(-Vrz1m}mp{lAI{O3G7hljGqUjdsJkmWHF{-FW4Ug-_-Od6|n zfV%fp;$N6JJv}(ORMvEGetW%C*kx;A+t9!!XmQChq#{x-k7z7`Bn&(dLyb`iQbC1S zr)j68q~M%^L=W;>^Yyc$SxKLGK-J7;-+;*(AiWoIU%3+cC`ss6QY z82vaGzdp{1_a@2piZ^a>ZmkVox@Z+V-iLHJdZXjiR{}Rx`-TzcBv^S-#`%UCqwyyy z1l|EQv0&WN=xB^VwcSwB=~+9(!r02xlUlBay8Bk?7V|~6x?8!^wQU`4R)6(#JKK$8 z&*o+cb&*q0VMCh*HKVu3s~2zb96TjIH{iuBI!fhlG(YbhGGJA>Q4$!)u~VF`7RYKz zV_~L*{hR4yR^d<$W$fYGqez#Es|VP(UNt{22~b^=H`+0Shr^GUYCFRbjK3wmcJ!juEpAvtiN|ieibYT$Ny7BNpLc9{O zuzQbF&Z_$%{dub#NVse&Dxih0GOQ1poKmiPc)F>TF#aI8qb@-^F78_YFuw%-^VI=+ z;$LY<^WH4!K4tHuXnkPQ=%UdRD_yyUSklK2NkPiU+}TE_=g%&Vn9rn zF~O*r{FMf#C8XdD=v1{l)TUd(OupXzGYRb@-HR)pH-0*q+3M)71`bDiMj5s%e)-DR zL}iE~ftnEx6@Ii(eq%v3aBd4efp=$f>#7G-DFbL((R_Z$7+?||r&USU-B+Rh#V;fd zN@D-P0OG9y_ERh|?3;^)B{Py#2aYmYhLe}`C<vN)S@5esL zKb033-&MU|-&R+DVw&<|I#q6m&$&KNnv^l0!H)#A^=tNJ3QjaE!(q?{J-7!PX89IF?&dpt*O2~<1I$Je~rn7#qaxUt1doQ zKy-|H*zp!T7Y~S+1bI9Mu{*lAe&kSG2(83F%y(K{g?ioA+=#?F*Tt19J$BD=>K;Pe z(5$E`BXj$5!}**{ZvkrUHTh1@Em5#(_dM$`JUU#=p7<9w1DU9Ye+^Pyk{=A#2|r$ZnVG@JJF`v9{uGhc3ZmpU!<;&fF^YyGh0Z>lMdnvrBt76T5bN8|sx>B09EETkPXLuGvU4W4^<3t}yHHLcl%x#G1BbcIe3P35&A>S8#U9l8X$ zjv(D2;1Ej%jRp@A@e(1+&Xp_Qq$e9 zMi+yc@b$=Lr#O6Dc*eNt@PZnYeNJn3Rqmd)t z4I|lqYR~f&r`lLQ^3Ap+l^?l3UX%96p^=Nl<zUM5sSqpu@aDQ4%=p^W#KS-mIZPNFhmS;?#XC z#H9a%VYlCc6{kSIJrVHVTsxNiKDMsQo>W}@9jsg*zhH2cXXfSX1&i1>$E*O^ zm*{h`Jh&>Nuii<8suUm;a#1~Qouf|yd>-@);FtAl`?DNvkw~&D4s7y}LOMAT>Og@O z_zSd-VoPxEIoSohDZdcz7Da5##E8mF{qqPn=ZYNvtve24N6gK`?ZNo4^}e(IO^5Rt znV4>1ltD2!}W0|M*2v*rSGoMJo((#@1&>OimQj3{oOLCC%W`8 zLL!%BL{3HRE13hWu>nAb9YR*7K=#==dTYns=bq*$T?Fc-kcG$UboqcA&l4Nj1{3(< zBUMFngz#{3^*|hlgj=9OVzY02Qj?e`o!?aeLLISJ8m4_~j3X%8tzL|C+Nd{^-oa6V zwpWprkqBDUqnM8poD+AgGaf(?J-j!&N0MN5xY31E>i>z6LG_5~jIz6u$7Aj@JBO9e z`iM*g?cs8Ra)VOuM~q*}4fLi^2$-;8Vm~GMD_@dfJPNhhF%sGE zkNdN>$oPw_j{x1wPL~d`^OuMK_~xJXg?8kG6$eM@{n8I?s_E|L^}?rvB@V!T@W~bZ z#rkA4jyfSTGqX(kf?BOcP=38`J@4o&knqg9`+NPwyN#uK9qwX*2Sws%xowK?yCcSo zmnBPFpUG-n`i$JIFz(aI`I4w*3*@$ut1$O%pFqj;Hm^Q13SPkLUdC5S9tWtfe;%xv z?C7GTmoFYdP%JMEHH-^RFk~466>ZRCc8evH8!1{K8_jW`0E)p^RUq^eQ)^A7GM)za z#g7Up{I6a*=%c?S8Yes^PX|3a0gV*&Axtq5av|v!V+Xt za=dp0l)6vI>4O4~`<_bIL8uTNi2u{s&M8tG1e9S0X`kYkBdYPV+^tCx^(o{%e5So% zyj^aAE}kEp3IJ=`awG25xzxusLt$226XfKG<0fcUU5WMy7GQZK_i)6_1bQF@P+%Qt~_>Yww< z+?YOGImUZ-n?))5;4NsNacrk<39DbA0_(jj2%PeSjS=SQa5H9ajn=#+W+*T&B&pow z;KjN3*bD>Zix_D|Xy?lnhRJ|CrDee1_LAXY#N~Uh#LJxeG$x&dK9z{`8@VL=Ww*1ipeKi{&(h3C3?HMGL{o&Ia^ zHzRf`V=A>ctVhKITPA#WFidEpD|JCQwXiZD$NFNui1%O+e~Bl&$NSK&R-kA_K>edQ zJD$tfXh3HJt}t|~w)A3OJUKNSKynAkkh&2=l%+o32=r5^b#0Y&cheuJ8wTkssE+v~ zj}I}I4`((vMw^cSRucjj*=j#kxsMc_O1vMJ5_IFW+JL78x#~-~4jyCv8yfvKMmBY8 z5TvVn8S_re=~qri?Ud|6w3!&Yh`yVN$KasJGL}o? z-k9=Yfet;%;MI6}z%-mkzJP(jEKaFZbWjhC$n7-T!%XkTdRxMX4}v+?<2SysBRe!g zWz;eb=0np#KI4G)A-KyKGXR|1>+)Z)u6cWsP*0VXp1?3>@pJ}DfmIWxi{luDKlqG= zgS-yX;7)^~j+xO-tM0EeXg9K^ClkP*fWW~KZie@J)3gQ0;PF_<#F4zX65ei zjpz?(26?_4^Q8)*)aZBKAr@XIc)I%_Pt!RGhP_D#o&q2`1#YA>c?cm}!f8dz;ZSP? z(~ohOPm#p0J76ZTW*0;D8=e8?z;g!jAo0UiZ5gG z6+Jd&zS>UWQHV|UTRQrEgD1OLpEC{q2`JzyyrQ<;=SLE=`uU7Umel*an9==R19Zy9 z0q5nf&6%^cBU5h?56SsKjV`I=GjQ9SNCR^3%WHXyT^&D;Cn|2&>Tmj>@3#Y-PbLgN zQBT-F<6Q2VurPg^)T~Ehux-&<^y-YpcbMK{pn)XY*^1Q(nH3m&y#-TH4Y)Y&qb2!^tqO*8m_Ou{pe<5`lv;J@*io% z65YD`2^S?vEH8h-O;#}ux5o-Z?IrseJdTe$gJ_%FcON0mCZ}xj016+UE)a|FGleeZ zkZ=-CPH>$%b81K(fs1bHJt0{@f=3{Iu6kVV3hy$vJJYfJT%kaoW6%;Shfv1 zC{tZ?Cf^Y9e2=*XcOv?s5}r%QiKyh3=_Tb_`nBB`Ll*2n`Ug4)7244UoGjJK5j}d$ zMjXsS&XwQ2R+Sgo0gWb9&jIfn@}@{_W(5=h@q>s{xMvNS^0uiD{>w$4tv?aVQ^0A!z z@!s#Wa}SBGpg(ihpe^`W?M3Mu-|aP&!&~V3&w2h}-oXVm3EMVF5=%`VNeCg!(soo0 zsy1JF=C*zIW1aT=9Z%>nkVpfautd03 zD+#Ca{D0)r+;bs&;{26ZhGjkT)d(f#UDkM6A3j$Neap{Y^>jBZk}9;aIeQ)6!6G&= zp4BQ7o*%`uWQ6NPUC{ro@ z5F=*v^Rxe^SAJ~>4(|zP=%FGb464ACGK5wlp^xXu5?r&bmBvC-B^9tZHd-Xpje26a z?WsQZmdZem&*WfJ+PS{^o%-p&%Rf+9k=D;LM({3oKVQqnq-01nEwy(>3%nI-6s6~El_O=4tPqBZvM!_^=lT4(Ut`7^MB~AB!WbQUgyW(} za%a6rB6?Yb+xOQubAvz{+sRBD4S{StGTAYLbvn@CLPDs?vpnT4$r1~Pi>+N#B2`Er&;d1)XLIwN=Q+dbC4ALd>xR!tiqWvy#qA z2%_k+L@3U$?%VwC{Etup6Q19{T_Al~sZGH#&f!1pUqu;cLJuDMzm|-O21wkP$U1fn zmwXG32`Y@FA?EPFh@Dccl;osB7%(D97x@aqPOn8fC2j*oB?NGg8<+;`Iva+;I;Z{; zeg=Xd{iGjy0=gM*xOXZ#l%o*DV-eJ*GkAK%(oV4b6ffGMJ2j8|{O$ww#O63=9}Pzy zWOLk)WG`;Q95Cc3^(yXZLV_l)sDNo67iyathH<}uDmf{MIBXIQ*%rVq-ZmHC6q39s zTsD&?rkxBsE}~G0%v`W!9Wh84Bp@ytG?W0K42>^c+^iLva%wtcDYz4&<9KrB9} zY?+_vv2RN*hvvnI_9n-=yRLAl;8Swxg?R$~V`|gB!;@Fd-p{TaxK$>ZDw_-!ZJ5)7 zz>2$A4@{0CY%!Ax1OsbJadx4`~k`1b-Pe1Jmgd{MQG6Gu54h90Z<&Z&*Sr|vpO z@Xl+`bS9W%d$icE88iL%_qjPW?1mJ|R@>^dII?e(I*vdZ{>UNK%;3f2pj9j;bo!a# zBr!hTiKk-!SpMB#JRgUB=%5?kZ~PQ~UqE6vp}tF1**vnLaVVg}uYh&B*~^zx>bU*; z1qL|v4P$9pQU0j6n4|*;8!|7Abv|D_RS|bFodtLhW+G5#JqW0jt=pLt)dIXe>XTxH zI?6L*MnzB07<%KDtl1emH}ph2u!s{Q0LAJs%x7PR$smnK8mZ}qeoCm@XUjkJoM8Wc zFWjF?y>Hvtpo-daQu=#_=V9$Saory0yuR4nGIL(el`1roV$EW+D_8Y z_U2W3|Lma4Mm5W}O&bh+_Ie26`7!@TB)B82FWp9mCc^K!fcFU1OAn;I%a6s>#S1-at}SRrR}x3bOWqIK>|xW|M+#a(Dz=i~fUN z#pCAu(DgEyKDv6-QVR3F{UzPAt5!?vmNhkejQO#1W0Ef890b-W&8m8Ok)6QoNt+uj zBbgurag;+l4(fEe184_48k(EQKyY)iR|R(ar#JoU7bK$6b!6-HAISQfX>0dH)xG0a zoJ#2dAJ1G7P*F+t<0G(_IK9!B4OHm(|u zj~_umj`s})8JSmyx_qpYCvj8C;*k~N;lqe?uE(Bi1G7Sv?VCYyZC+6;ZXN9oAH-+J zjnFGw&Jg-u{B5b^gyVbT(X^>L>H4L*57`d=;l(!t&Y0T!w?b*cft7u~oN{v%gCfX6 zSgtTKFI+qU;w)|p&IJGWtKLs^ne^&7Gq*@VOmJpi2%z%}(P9a_%_hM&)S1)AKAMdU z;s-Kn9;!bEiBCzPcOhQ8^Ws^-O#N+JVuYQh1oLnTgLXYs^Tf=R)S`ha$c-|}NZsM&Fz$;xu7$2%+434dVr!;?$V?s|n zcZ!N&BT44PHhFqkNx=iBdNH|NU{WXk29^%~4HqC7#exb$JV2$NAW+MaF@)G%tTjR}BjN#o>%$W~V)#gK;%$OIIl;a|b^x3&(gaD;U$?AlUCd#d_TOjT z?P+#xF_V*%QN<+OWcfM*P6#To!KOzDf{%7V`%t$6Dz@>4E45b-as~)*9335nxA3Ul zZmCobu5JZBg>UYGkJFj zOUa^7Opw0sZ}Qhk2Awj2CVH>lbb2t#5*KDwwAD^~K+!d7GpcaqFz!T~=$cCQ+itXu z@Nx|^;<1ptYjnQ@NF%lx714uH1$s|X5RoPkxCJ}!qWnAz2C(hSDYzwvOH?k-AR!&b zB9Gvzjc_M8KHmm^5>AobM#_60Ab&hrru<}3$XGRQ=q%&^0(7{YWu%0Xo#AO`IuJHk zq6>s7EDdFq~p`i2*7{BzkGt)d7m_mN72WSRujm$GY|E= zDVJU=GmiV`ig;2w`!K*RAYG3Xv)dZ79!$)lj(Yo_S>TIb*GaMysrNNST$a=3L&cNy%L{z=Wxlw9IMF-~WP4)w?Tb z>I86hLP)dUyguc#QUN&X=t;kqC_>q)tG|X9cpX?|!wv2SDa_52<+l`Kq#zq;z}@pmG$M-P;r75Fk>*J_?byX*$JDR1oR8Fl28 zeg9b=Ejue{mbDilf9UsF-Cp{b{_l%J{H|58+7<{50Z%+sGrQPct!k&L&DV)UzFf1- zaL^w_%^lzsc=f5EvbQr6H}1MWj{b1b1+ZgpG?0m#n`YmwQ>Ay$10GKhBp!zKYXqYf ze8#9%ldqcv1gs}DU6PGeqcPipYzxg;4kfb`wtDkdQm{ldh(gy)QuOVZGHvCp-roKF z)gN9k_(PZ}!&$9OzoDBfgsH7{P2UC8Nv&L}k6mODJ6B#IAJpM8De5@aq}ZC2d55 z5?1S8DgltvMDKn9C2c4TC(xtYMw&hX!f@|v*>5PLxlm-)!GO2Dfug$cV=lFlafaZp z%36b5B#SZ9lLmfIodL9Z5w^n`HQ?D!1SyOBxa)Go53^#`&4eJen2?y5>!O$xv6z%_ zqW6iHZ<1ne#Z;>=^*=ZtxeS2$*zJ)lIO`)nA(yBhwA9q>Y6pc|blHiq9PTABgeE6_~XD%?y zXDsh9Jj{hdDJrYfEftbT*!AtTWDYtzGaOKJWGNb|S@!LrVs`C)TxhYAU}H&}qGclS zF@MjFI~--z9$rM>D)c6n(U>)2yX!9ZyN{D{P6frqjaAh2>$T`O-Ks<#_e1 zHc(s4;g)h`@2igGh4-O#OU6 z)|3}QFS7_Hr(J(~XlQspf3PK>r2I{Xi6lr(P+xdtLk<_=bb81D@!O5Rm(cg#E7J~D`RZw}$S<~OQMG@xtxB1O zxu-pyFROM)GsIT*cS>o{DMbZP*7kdA==LT{_RmVvr@uYU1R@f40!X+Gr6$hTJbF#^1#35%v9Db32K>}mRVX?lT=UmE=| zxsox0E_vdUQ0|2^Q3H6WZ?cq2u$C3F4LiKzhZP*F-TH8;vnJB?! z@oZyyKFQp8_~dL_b6oXLe+ZA3QAUf^0+*WUyxh96Q4=Q!bOZXSO{zLaeCF)R(2r}D z%O`!qvI&DuPb%PMUM6gOI$g_zRGt5Xsl@(bP!Mr*YXUV$-3Ug;^aT(9+#JBKFc6EM zuAV!Ye}W+^l4kg2P`w)aaJ3+Nh9~eDi@5ue`W;d{w3(asB6?z#OM9>uY8)? zKmY^ba5XE`X1~d>`}pcXy4j_mUjJ>sI(_BD&3JAOjv^V9XNzi$i)){Uu%>HJbJ^$| zs2qDB!f!@e8#mHP0X+wMsNeg(EtO-UHDK7fx5eeZ&o^VMj)T_<99h=g8R;^jTf6q; z+c;>jvqc||D-({R3e6u(6*!?C#+s793%yM^9Fus^YWVgVAaMlp$(J27DENcfZZsWB zu@lr10{vT_a@6jjlJlgV>3=#i-rUvvdpqEexu0JP3@SCzI*#c|IB}4?QrspTj!C%$ zrB>XcQz(c?N!cvD6cifVV0_;0g1g_xh9E5;`*%R``zyLDP*yy4dpC&yudh5a>guKK zq+(X-IpNX)t~=RtD$Z0=M$r3}H543kuVON2X5Wv5n$2du!DI5iQ#-!z3Y$=6a)%qa z(Di4X!8&pr)Tbcv*V-G)QwbBJ{z~Gw?gzs>!yLpLmi>KtO=vYio(W(Wb50XdFjhE9 z><;`S?c|B}|Lptw*eGfXv7dn@@<***s=6{c!he)!?xpFh! zb@(X~qcO9~iq=Ungv|2@IUWIQ>|-M6awTkYyI_QMsDA~eCh$oBG_FFMjgYIBAZ;{Y zMK4OVMRD!6#4oHu$0tf}(UfqBJR|ie5;LRt?oJLoNw;vCX~{yqKAmnyMV<$)Ziz$J zHVA!skkpp+RM_G;?~kDo@O*Hx7-4~&Zy3oP-zZfvXAt3g*LNU0ylmpZk#=vml5mas z0mSd;gq#^M%8)8EoSX3KIj|^U zxDf}ZEy~V2{Zbm1T*iNsW9defM&97`)~mvA-Fw+s@#gK23|SJTY1h3gdJKDVBooK%G$%T^Y{kRG;6<`8?@geq);+E!phJ zNY#^@R)(6tL6W$wMy-(dd~;7L^UjiDE$Nbm3)%@t}W>#{^8^Isk8VQ?yfHLIygVDW&evi|CU6GPfhzb zs+p&)bLzp0r_+B0d0LE|#Ctrkc9)lxP@GENKs)_PIV;9FZbj#LVgy}V+_6V~Y`bI6D%{kdUwFT!c1F+`S( za$LqWS}tx1JcB7YUX*glor{e1nPUBgNcxsJ8yovm{1?1iC&oR$mTeX>ib;ArfghyF zT`tfIr{(Tv(Cj1Ig9D5yc!gq-0#N4Hz~4IfPh%Fz2NpwfzEnRhyx_+U>_3>!6OW&s zOUojmWG_}zZX%VBKS*}VA9K^~e)QaPKW-u`bn;|=`ht_R=_@2Xn;A!=mWtX(`jJ1F z8&_|mi~7Bg(X}<_QG%liVSf~U{1fRK=z8Xui9jl5zQq=U=*tjxvx4VsLC=FpN zyv@ld#&FsPTU=BhK3NLGm&(VGS5gjDS3E=K%Eo0Yi(1c5E><NVgOHCFZ*Md9Ol_9NeBVy>txDi35gq1fd zP{)i^KS7g}f_MU{t&@KUU1pIkoQ;&r)y*arV*F7~bkNRkYBxuOb)dYx)H+p4Co<9Z zv=2R1GU8I;N2<}%+g7=gcYC-0@6$#4Xy|bVv=DG|Iz}R~TE4=*ojYz1?oEuN)bZYG zNB!H*wT$F)OohVo>ys#0n=Y#iUG+3n7^r%hUX-0HK7{=EKf9XK83GDwCZq0l&Fcv* z7Z|JKEt!JSG;A%d8Xg;4VFqRl;zs$~zsrsyjx92XEd~&X>ckM)FV! zOrbvImqK&GDTDxRsi?-J(iXB4S4uP$s%Si0)LMcXylqcL(hH#k;C!Vf_r7!ZAHX_x z?hoXukgUXHog!Ople|BQ3UP$p4}3*Bd0d&HlpL&%%456Y3?w0!MuQBiy(i3tTlA{( zR;KG8|B-XEsc0=n;25d$LZ>@uztsR{Q{lu>jG zgvx6-3*r^>g>D{#(CR+iJD8wrUjxu~OVv)GAo51_e|=K-rW%KnM`Bkj%dR&wJ(u zQ4q;8bI-YR9zDMaNoI2Ix#v0Wa^6K1p2$AX(bpYN;ighS6`oKyR{R_(ulkwJ1_;tQ8c|6(oMPyV= zH{m@mAazdOMNOX~x`NmkuS@6g&3r!{v6bsXQ`4!4t{@hQbqU^DJ?FaU()|TBwTkEp zVqlYQi>KpRPS!PqF>0C>(G`rrQ#uKIX5ep;XP!Lt?bNg@qAO$uBXxrRJi~vN&fKZg z^*Z$k6wwvZhkQ1|Y%DPNI{NAx&1343D55L0E>^NZvBo0xg1b{E{MqX9D55L03=ipK z8YJ*Fe#Axq0=A!4_^#chARdOX@u@`D>t>??sS2;Bs>i1!y27y$=2Jn`<@(o){JA zqo9bMai~zz%!ckND57T+DmmnEq%#>uK@mNpQ0bmWM{B3oDk!376smwj)6mj_848N% z8HXy#)O<9j1eSs#dPbp&EVka7JeH!C3X13%1$od_+2q#850$A_P(;r-=q#Mh#8u!? z`q0>TVx*c1Mf6PLF@5p%Ra22MP{9p~u*+9d(R3(D)HDodqfAYMVW60Kik)A@#>r|r zltgcBP}|{vBDpXaq-#_Usi{yzZ!J)6xF4xpaRsq(zfSm}YAO`bTe^sbndzol+9q32 ze2!GX4yRhS%0@p{oXEykvRtgDX|P(Yuv#=&tTtHKYcv`~Boc$)?}yJv%7}&r_8N=F z;b*TyhYUoV5#G9P9a{lx31}81b391q=JLV6$3~KUt`>^{?0rZiQbXL0f(mwobo1&Q zwT!>FPFlLd*VfjWY15;lGGV>CHby?D!_N4YMAiOkY}!(e)nBc{HygLGf7c)w3?mc{ z!$!m>k}}e3dITv45D#~-@q}3xXF)-MB6=%;j&6iHr?oIr+8L?sjNChS zRpOJS%ka^n&#-HEB`2@dX61yoSej;ik%V9P)=Md(UwY}cL_b!LbY~ia#$J<77ZS&H zNA%ngh@Ejd5+xmaL$+x=d|abF@h(U1`s>{0z^h%hD6t{&_W@j zCH^;T>}R7RQaK39M$aNp!b!O(^z}e5V53tQB z$p%S!`@#jVzx@^`e!Np>L?%u`?404>s`{A+g(*|A%se*6DjVJWsyhkA3K*OnMe4T8 z?ZlF$D=>ZLYuK~53NDw6+tKMpi&gjLfShKy2Ofd2S+-?_kSMJ4Mo|=KR>o&4~JDY4{OG)%Y!Pc7=2q4j;2SUHN4Y7d(p!s}8 zJPum;rEF9x8{HfrRc(%u7m$H)oZnz<()+aJ%Mw9wP>99N4=&EdVc|msBUKkVen2opo^*%hz z*0$=$UgnHhX_kWmNj4mBy#>eY*@zDugy64!MIOgYs`XuT={^l9=d3AV*2p+2T2N)w z{n_nyJocAqxb%w2_|Hd6xI`!7D;!nrfpzmH7 z3@jz=ayjwTvoB%vl{a9`+OLu0@hAc}(SA>oQ*!Sf_;boGE2%c2>d5Z`SB798^axT9du^~ec`rYq< za5$5^ATKi&7U{1fy78bm%&<`;cRHQ;@~aKFc+6yc@Zn-b@N^KK&8cnudU!{SfOFO? z?#Rm^`ssW5?-bFE!!o2YmNMSJkL>MGD2(f-+>Xcp@*H=xDSoG!@Ln(MZ@!7#^Ui~1 z=dMg&!T&2vU`M?8*1X$k+*b4Vy|hCsCm zr~rvu5j}k{w5kXZy1Kd+qpr9Ci$7h?9d8PmM|AXP9^~U?W7x9K@kM<-oSEHh-8$Jn zmTBH(aK@2pPDf>VhB*S56o2T^zv1)ct9hEJQqv7WFo@9Yx5MJ|;ZHltk!Q2vH`}(N zquqg*`u9gDk;r6QoHKR7a~B(rD~aARP*3f!ktC1DW4LVG4OsoA(jcEy4InxA6cN z3ci+7B?PeKh)# zBzG~XKjy11@kwo+5vn}latPLkPXwY1(Ls~E>g&JWgmDvo2B$-L)P;f7hUlf2!o~>u zKO4W{WIq;=ivMW&HaxI(%Q3$v4G^ZYu8~9+rYE+O+2b$Io{NVc`5O;@Q4mCB4+P-< z$4lJXzBdxV%KG}I?GbEqSjFCR!OE4$$^4o7!P`Kpno+s_b=DBQSeM}c6k##ty6bGvZaxG6klT|qFC$6^RP@dQ6JCmrjB-4(6!xn5@Pk7t^}3Z~8ru&vKU%0u7I z3`8HP6a42QEhLA-fzjh`P^!9+EY}b(DnhJZKkhxBuy#$WI@5C4XFjlHGnO8x$2e5q%|6N&61b7Lmn5B%cb8s{A4*l1YSr`cqyCI+tl%cW&C4Hc3Q@ zFn`;%6La?M5h*)i%j%~^otxM;u<1#eazK2!t2dZgc6nKi3Gw|U(Juj?qwuT z_xU$D_@3WYfwJ0K(VVeVNQE-wysc>?w0Ltws*9ztpp?>4|2>6gp(zNevWG(O&6>rQ zcK`?CaV)8;%SigEo&Al6w{Js%-G&prxgt#o1$0nhmCx8XRRoDHbTGKb?ZV|(O;v#~ zf+KqBX+WDcT#8Rv^HoN3%qfcQ&W#(fJropiYFeeLjj79{P(#9NC*Q6@T1A)UejkDl zKg8qO|5;g?>gAT{wZb--DeKoGYSJu4(-NH;72fsBhY>bDKtkbYVB`+`;Sug_cQS(i zwPL4XXWY4r>?2oxVMNxOB6_oKim77HzxXP)m+w+U5?YjNNE8+#Hh3@(Z@qrQdU!3y zl8n6l7cjEtnvCqGBRXkbl!dnN--}dOtLQxU`4E^sogb&I_4`o~633@|)7KlC9b2!6 zo*5y5Uq1Lp*zGFnP4ov9o-hH3#kd3RW=1TlKpXQ$w(%_3U!{mHB62)#jGy!id zkP+#<>S~DS>!ysiQL9%Aw+Ur6(O0ZogKgV)DiTScvd2#vfMkab{QUabwQG3nxv211 zE%?PZUmI1DN)i1~IGuL<_76|;-s1{_^W4{fz~hf08IR-bs{K-&Td(o?up!__QW4!a zxSUR0f73lGyj2D;QU~vUfP33LOp5=0`&P&xC&@|pvke=hK5~CcgYeIo!IM|Ev#&P> z1Jd2Y*MR_bhC+(ynFMuDx&7YXDwX{xh%-Al9FA~}Cm0N+Mksu6V8v(aSD7*c|bO4eO+ojC9RY)~}OF6KRCBQ6*cqm7{z|1+N&R08Z#wJcj(d zJe<(G2TtsF0{ZspiFR!Zk>4f{nnmLTr_s>h4E z%a*~iXAi{`^J=WkB|mKTF_L9eq(N|98QaxLdU<4)U+@2Bnq6>yt#9Iw3w3tQw#x0{py*hG00fv28AqND_Ic1a^weZlQrnL#a~mYgz##{guCrv(KC z*(cH8eXlHQ3p)mxy4|ruJ6wO=IGi?YFjvos+=M`&mF>q^3|h=bsep)%I2X|iFW{2h zzGMlU&pZQ5MFn@T9d$$M*%OJbUEyNaIrghBx#})^A}HO%eVaGo|9Y@{lO(NGI*IP} zdNA^`Nyy0}+dPp-GSxhc(U+Z%N#n<|r)FUsj2U}Tw9&Gq(6Ro>cXQ zvNCQGEUl}B8IZ{3k0&?(_LE@tQGsXYO>IaoP}JNzPXxyH;wSM9#hN#7T7Vspg2I zM)6=13L(uknW;t8-pZZ{+3QMmZ)Isd>(w{$-usJ8O>nA+Gk9QsJb3@@LX-QbByhW- z)znZ`PVA4yaNW8!8La`ANpxfP_*W*?$g!b*) zN=tISD(O6)Juf2n#4lHwmgIyDe!mnBIVqk1nS3*6%`q*>={W3!%J0x(^dTRx2LZyjgf6Nuj#ax%nkyCvzL!Y@n#MU*2;wh7KNp zsL`JEG)j{SrmbANey!?~J33X!**ocoPQGEf8M-|9tT; z7&M@tX-Q5XSzgSZ-_lct@Gw%dWhT((HDhfNiYA*}Qi^MficKTg zcZNg4hWH9i^z|Dy!)X#HoXuv(^uIp}zasfT#2CRR6c^*t_U%n^KNRXaziOW%dXsD( zn+?xA|FZN+5Onq7P#CYwd{UA82grVF=Pu}Lk{F5SD=LJnYAY_$?KW)LQZD_2CpCS} zE6*x*{YPx}CwreDeV#hOACL2)h<=P%vu>mGJ0CkBU+fTWhAJ-x<{a6 zHsOd)^~bAg>ZE+Ql!5lAKm0${BC0huOt#TK*rTU3rJ4>~$84jo5lE{Oj_BLVcX4N& zj40~R9wo}l-g+uLBl%Qjugj?0QB|GyshpM~`r#mTebM62VYA6sdWg=wk(`FV<&X^`rZii@SZ_VQ3jm~dF3 zh)yk*D);P@?t#YR3HUh4K9_KC-0*v5SjJXKv)I7cD2@oW=-4^iVhq z+4S}#lQVg_xr%`qLO78`C&{8FZA^e|4F(m_8$oS?t&)`z>fW`piiFD`V#x%~Es)Dj zCr8|tU_cSQ5j%G5g3Tru(N9116h`!zVqnHJC~$|IsCGIf;*d>F>9sz+b{u`ZF%U5P zR7MDQF~zuV-?^JhbXn3C!fR@pR&+1hvPjSUK2hOG33cZ)+hbVgidDqcN@DM zZEQ9a+U)4%aU+lYdq9pCh3vJh!+|{ZGmHKiF-G=?!RuNGqLVXgcjaC{qC6#%STormNuJC9j5X zk|@r!PKzz2#wDWA5@A@v)v!gXVewbM8f2Pjppt!WA1skt{yRvm zB+;Gh_k&bXMM-oE+fjp|uv7~!FE5wd=?WRvq!>}3>(lq(*9m;9IyX{`RAx?IPA)C= zf85X1XFqJ=Dn{77oV=Dm1#C?EYK**Ete(9wp_AQ`dT--6Xl*wxSVw9!$KcO0qHEuk z=#1!+C503gv|;N)iF6|nVk&gfVrOb|h!HdZYiJ*nzEzC4q&DwjL?*SlhD&8`4>HyH zTjE-On7nCYsiqqi%0@>x65)j*WkI{Ph1^b8$UN-TE8t!CXo|GZ9bVcImU3FjE0H)C zh3KidkV`sJH5}EP!Jwt$C}3)!TPM0KFjgoCloKk~X>@*@JVkV)C}Yb3)ZEKQ^dt{L zkV_3#Bv&|g8D@bNk0_$c2o>Y5w7;fF2Bo^E9-U;Vo}q9=rKcLTXz3_Swz)U>dM%3N z&BKBE27dmpV8D8b9FR+@Gijc*Iu+4*Jdq|BX=-Z^2-QR{t0a1OzkCwi=s6Q&!vmyi zyHi_N2a8on^vp{I3v9k}3GH-?B6=c`fX5|Q<&XpQK3FYQMLYxaKdW!Ml-I3@&WJ8s zV?|0+RYzT_!AFxdu+BL$NP|DzJ0!Cx#bsXe#3FQp02Og$bA`Del1A z56gso`}V_TRi5{ZX?_+5>@};TyY6^y4`rh_qL>k#oN_Y3<#J-juHA}(8Dn}ZH6**{ zSY9vVO-)he!|V0(dR{VtYTdmte}S^oGX&?}#ca}@k`X&UT9Ejfg}d=+db~zV&CZpsK1`5i#xdxgD0EeBE|M zse&U6(W(AgpFTaMdyaOS4S#=rmda#Hx7uF$0aM#O(p`7LUBq@N)lKy~7<$S89y}u( zK3ld5E|)rHOPe%jcDZNkOHyX6MGH=ZmWV5&ABx_`_dqxjmhNGCJZ`-CulH4yT)Gx% zxW>>fC(YlX;)t|A6(Zzj3Pp66!vTlgF6CmJKJ#_t zakM5J(Zi7_dK}kH%Jpcs+pu`)3RRr6wS{6L0b9dHiOmkTBKr410W;?gA137krL*)$ z{`3sIUM1CAZ&IzuTl+h}D(CTKoJn(0Ob&}^WEnbSAj08@ln>VBbYjl@ca^Q)YD@{k zR<}-8S&mT2NW-hYib8Z^MEp>1MPWg{)N6Cvi?2!<_#>Xm>qMKC_ev|xX_JbyJw=$y z5kaC;L_uR4OD@e%I`IT)7pT|c!EJZ{1~!{bv9$%T*RFvjS|{%fSZqkzy^83K+WH}6 z=h+IBsyc4W#Zs%kyLRuv`mZ-Bwl-Iq+j5Zm)o&nge-VvI^P$jUK}GbV;iUe35C{aN zU8t10`paKE%8OGdG<8MVk+b`s2XMzPrsDp4ZegZJ zfaljwd*wgaxBq}*@Q-P`b}tGRPn0ro<@?gy-(!+^7LkY&oj`Tddmh(Sn&(U1Ro|HP z3L~ja?X9NuVM$yD{DRfQ$T-1OHzI_RLwc1fQ1#l5ilcbey(9Buf0DWW$6SC1Pdlv9pW z_2asC#@lmV;OA&ghaK;JxC%>Fd;^F5$D`q>pyUg$e3;b@8p-jN|LKje1u9H*Q%U%H z-3ToaRzziB71gsl%;z{yL*sVFB({pNw;c zpUTh4Nj?9^+(l`r_)41pHKWfoS*WbWqula8n<_+s#B|+br2T)v-*ZY@|42(Ex-?>> zq_`uzIXMW0Gvn-_-siJt{*`;k6Y&^!?Ww{0i@)LlFln(}8NX+|{t<=_=!t=SyCD*d zNxl4&Ru9}e=D@x6ALf+e2c@|WzTP(rU%XP9LtP^HBM;w)Xe7NgvME5PQ)v-X)nA0w zYT@VK|NdkR%069}Hj*Dy<=N-2TDKk38R32Y5NuWn&)h9GSi;pP{NyT5bW@X@N%L^& zB~nzVmC7*e(7qiybZCp(y885}>cJpxxoHy4A8{u4suR(lr=u|TnyHjiQsFiB-@Nyh z!R>b7vJt1i?Xn{-y0eZtk|d(YTX7ex!Ad!+wl>*;u37Uxt6a`eri=I%o(nlD|0ZF?if={-}2EtJPI?xs;q}EXJjNNyh^8>Q;=>qPE=5 zPci~($%8dFo4%HrN=`)9q76)PuY!B`LRb%MND-T)H?p^4PQsRhx?#^S+dO08uGvcT zRKK&!ue|YKDri$w^`egL@zkGx3!g8*iT%NnH7Kvx53A%g-pGG3;qo(Kv)hsL&EL6N z8O!a(B+qe3I6LqdbCSx*Z>x5ZoaRn6s<>(i*Ks@BTK^5~^{~r%TPFcTH^5cnj(z@?odcw1tFeD;Qo7WIcgefqpH4O-VCPhdw8T<+2`x= z{wH6WisXb2ZQJnJ{FLpea^72au|-ffV^s%CWO0|w&g`IZ1o$>QGZP&mA&Jcys(6k1iALFVzrGr-0HsGBPR+vtTH-gU9ON!dk-NU(WF+|R!l2T-<|&=a&p|f$O9E>e6MVkX-VElbc&#(!Vb3D zHHw_Ag;)T!r@tsQ$xS2C2~?^_zC#i?v&`hUA1UC2hGwE|ocES1fG>yS)D7-wW4TDXVSt z?_*%UZdujqjO_C+CGGjw_S)6i8GqiYqTGj?(_bagRn-p1FpsokI zUyFE7C)3@Usa?^zIc}UWw2$daDSKQ_y+<8W*(IoUA}GrdX*UF+(u-5kM~dijp+}dF z=+U*KsqeA4Lq3loutch}tN24}nm?|N@b&p0M)H^`@6Yr^ColY&OsW^=d(A3{(6v*0 z{srZYL$fK?HhBsyI56bTnanSjlIWDQ{B+ZoXXZe5`l$0#MHbBlswr30t{~-z+yA*@ zZIf9ZL(N&UL`_y9^`gB<68&bRf~z-+`aG#9!&ReB=k1it1WHSFyBsM;T*FpH+$QPk zVN{>>1}ssZh*w9gUq~|T7g2TZ#YPDZt8Q=@3_0YcW4G)6`z%wfRmwq6^69ofi_|Id z9fH(AS9ge2MJWk2~k6eavM7<)Fu63B9(s3Vl5` zH7JalbLO%+6DlBPht9{e^ma<89y?p#nP!XjoT=jLwPVjR1=+ilw&Sl>-*)A3KNgdm z{YUD$77L1VNkCV9q;03pgu!}^F5xS`cXI|y_9(7nWcPUF&GMp%xYD9j(Ll%kPZf)Q z#9B;hcGRBnPgul}&uZ(Ar<<{u9@ecTNJq)=Z5jB=B(Ed_Y@nc>6GaNe_Vt)5ovX-(kJEt z!*`xYDvGXeuTDH!L`1~cbrrP9*~;&2izTYtG{lXd-##y&ji-JsMlszAR>V5(#@AYR;a6Sl;nQ+2cY8 zr=vmioW@2q=xjDVGU_72OqIXjtO3|xU5~dv`~r5H@ey^!9rYaa_<>ZP@~i{nRost2 zHNj)WEu8KJ6^&2+L1vn7iM$8g66RdAuYY*Viufo`2T7+#`^JnnQx%!@&08_|<7 z#9bXwd)6BW7Y)}*o>2(pKsFQ68)5iS1=;JTpK?4Vj5(9nkvBq}CrMt^KA$_{992t^ zRcGjo)D0*78V3eH!jn@BkmAA|37eVdjc5!$Fhs2K`so%6CR~0dMh+jCanF35a?ssQ zB(2U=L&a=R(_?;wI*&&6*>e%|c7>MtrV2s@d1VXH3Hx;)!&sv(fYkSPg}In=)w%eu zLA{u?&&<9KY zf$NP@=cx$g2|Y@&ZsSgTwrZmuhn}7V*`94KQSKw~=`EN{NbtBH(Y8b2>w72feP%!i zE7?P(_-@vvBDw&Wj8xhmrTiIEn48K~9pAke{f_U1&D-|i5qet+KU%z&dZLKPcD`0-kY@)|=eEx1ryTPwqTzQx8U5iW!rY1GpBQ%3rV zz16T;ZH)NGZfDbR-t||WjY!gk!m_dQm5(AKTO0^>9fwfoQK=&gW1A*WF_g`sPg_Aj zfs*LQfXnoIrJXz9$O|FI9fj3Wq#Z@r?WwBcEuMGpspW*Hv-zW;O`eAz=V^$hY*-nr zDN@C49t2A-M!576B<$Xl?5nBAhE2P$qp}*I zP=tT4TZ!I&NfbvMccevllwRD^6_F07Bg}}MaJ5SjntslP7B{?d59Lj?MJjwX>yRxr z`q8k2jnjmEKx|8DblwGmVXoS17Aw4KAA!C0D{%i2k9D`CulNKv5fXZ4Sdt&<#l2k- z%kPhH+aZYO_CV5VXJn2TCLC>% zwCAKGG?AWf0785H|BsCa&9Fcf(OVak1HV*a1uZ4j*+`OFPha!Ln>d`Z|<`Htvz{@{`}%#E53&^NZ+qxKAzR?SBw${ZP`ra~wP*Y!sc z-5mHAGTbjq;fNsbUXX(dm zeRF+PO@$)55vb7T@Om{BX+fnvzt^a#P(&92FX|F!o|=kQLr*qJl*XipE+VKYx;}zG^C(goQf6pH@?$h%OU0=nMBQH5Eq%#VJrk+eKN<5*+!-$#+MRc=(`ejqYNEM3>JfLrWsYg7t&he646_aT`;;=;RUaySR1sYv z&A3LF@Rb?-n^W}lqDp8fqAO$&3Eg&2cag+-P(?8n(G?6wW7*@ITlsUzgHE3Gv1*DH z(G|qO(>mc7w!&x8SvZ}EtHu?I=n8^@3OG=NU0pNYdlytIDpNv*z?u^jGf%PegV}lIRK&VwEnTANYZvQ%0Fbkz5g7!5ny4C;VrJe2;R&DMvi0rbnsn3g!Zl z+Y6N&o^|;D0r?9RUeViik9+qF6xTvjKm_N)c3) zsuU|ok*dH)QQ!llL#QE;y6Kx_`)1qi^*`^Nv)N5ZAldBQz2*7gcuTh2oik^id1u~Y zq|?Le;MCF4!K*;X;4?zCt5mz7pPv*2nC%GGB}8P@-?c3Y@vM_R{TPy zk>Bx>{X0YUXDH(3AcTTW2-$J=gH?Z1#5Abd&s6gbCi!gp^D+@aK_`T4 ziQ5*;9$PTqhE-HMTanN9mi<|K?9cAzWh8`xPDmlLhiWgP&mm2=w2O1_?w3p`a5oXpB+qUe!`|yVrh?WUjyO>DS-?V1E-rK__IOoU)b0_+?s*!-Dkr zIoNVXt<|3P2e|IJ`ISVBQ3ByX4p zD=S-X<}%6F#>{2;k6h#Lqc1bxN6632e1uTYN!}v4#ean@bBpi6ZYg+n$CkPCe~sMC zolgh_o#ZuA{p)S?RsZ@aSpAD^9~m1zL0@&dcaYuH?FgZulf0$3K6eOx^||+Bw~zs{ zFZ$}w%b5B(hPVDaAry3y*UVMz<$jayHJo%wh8Bxg_ggH!VvjvSDCmR?5ZM;JRJBff zVtrq=&$3&{5Ob(K_GAop*<+H&IfQ~v$fqz%wP(=hxOBa0W$YG`C6wA@ve)7AS$oV9 zLO~}5kAxOogucWei&Z;@-9mDN&)H*E#%`NEmKhRBDCneMkmMh|$8BKhPLlgn z*<-mkY0+a~(jr1ZCwVx=zK*`)8j_xq)j>$!!`&=0 zc&o*9Iz6l(prfOMS0Q(hv+v)5&OSp%@BL{Q#uksJX~KGH*pKLYU;UR>|H;={uYq_X zfoL?g`TNyBAnozUFW7QINGAJUq*B(u_y3x$pBpqRGssHSegwdDq;e_K4$EDxDK>rNG>xEfY zhoVa_McVD|6KT_-L|(m4rM2KgJx{w_>d}LAaWT?9A5c_;bV-SRFI85CbY&&d@~9@V zwEUafqrXpokNnND9*~`7wNNnBqCzhc`Y#N)AlOk<=ix7^T>&dZj!@7)1}ABU&jFkA`VzE8CPu!`LkR16h1~l(@=gafn#=|97^W2F^cbkDJ{1v+olpkm&>Vz zJD~(8pv1bRtsNUSG@x$fTGX#^P=ej8EnwN&792{@91f>`GiaSho(21q=EX17fwyLPDOa#Z21kRRUkGM85{9apRDjHXX@{ z6ObM|R__w16_0E^Sk}I@!WDd3a6fH7mjQPRtojC>yk9Bkq_CK$+Kph*i7iA^&YMNF zol0a=$~u&7?)4iR@Zm?xux$BCtW%;b+udSO_X@mqmfa`T^4jO~X16G3%c4MpShn=V zg6}W>otWvdt$T2({aRK`m#Fp<2O9;QC$-l45)GRRX{$RFieS02wBLI711$V-DH@wv5K|VNTg1KJ*0+eZXB)agvQ+TJ!WT~b{))UT;D2T}VZLe?+mD2T zPO|vMy~MV}iwm_A+-?zX7s8P+R<2x&S6_bz?=M)aozg_y-EOzGxUtzvcX8z8D-H+J ze*b2x-uc>VaK8HLW)V-9mLj>^Zc5}2Me;KTAw6;=(y|DMMJ4Boe&ciKWP-t#ez z5DGdO93(tN;=+$DGy!)w#LD()Pqu}Nmg1@Zyny!>EYfk+BHF#ycah)6Q9=galJ{BT z%4}$W`!9ci`wxH612Z*Y0^&y>t((Nr%Q?M{=g%+1V7T6Hg2cOv7yS|r4hlNixoaYs z*(5_-!PN;D0T)Yr)tYtq_tP)pwKw0@vE3rzvb8SBTbxb`I2i_PPkXba1)lrvgXhjW zbrKv2or|6JB_s|#L|gd?hxK-To}IxT+s~+wOm&j6PBPaeI4~&aWJ@GBy5vYNxH{oF zZrkh8sZO7L;Z;2G@8{6m)T-uAuNH4@ffxBXLP$2QKJ}KoSzizD_16Jc|5DC-kT~Q} z#7;dGsTsRjmV5qf&x@gPo81J-H7_~mS8`BL&@;UEdp)VX@j9HZybMEG>dEQT5j*D`WxXG2l9|P|))dDU&E6B4?riYah#2y3Bd6&wn5HKlnJ-)~|=h?bh+( zBJPCb*UakFMQqH*MtFbwTNsyJhR6?ofY_O5YG=DVPHxa!R#}?-kq^QOrLE_nprGe6 zhArOLqCo3}dp%ZqmKR^0j|U!p92++_!RPnEp_IMI&uw&40NCYdL$}|K#J>B&2n2NT zo`ll6=i4{ofU+{ws{16UzAg{VS(yXRvKwqs|FJFdOxHv~&xzHkY@?qCYg@hG5KBCr zPT~0%U)Lhu+S&o1&!f-Exs^@|XE|@^E%(&eu?XF815%Y0TFgBT2ioItT)cc4>cvu* za=NM=UsZ*#jv0%w`77?Q4V(MzCV%2xu(szLIbbO0*@B!Mi9_4eLhS&_cP>rN!tega zzp!3e;u1BPI;-T2XpI{h_1J8y#U{H9HK zs;x~YO!{2a2wYHGgE4MP-1F3w6@%yNb`!**P8{nVol84fZE`amdC?H91pthx)mGh{p6FibrE!mGIJ{zuT$6VzMkex_I@#6rrqR#qZ( z(@j?PxIh4ICFY%p1irO&3D!m;%1U?a^!B;f@kQ$UVoJbosjtViYt~>!Q4ua4KLK+~ zN)Sw@kjj7mGBKo{?(Jz<>1f4Qsgnbbg1$WtgH>*HF1x})61vo{Z@{1LxF7Gn|DhIe zN%kU-tN~7lExN%V5(gZJ&@ZooA@@Zzsx9n=%0id(^0<1hcRPKc=ma*hb?@8|K42OmQu8q=BFde6nF75|dBqXBU6Aa-Gx8IFdUVBSdH4_Ind7lvSv6}`)JdW@$uhv!ALue%<}+8TXc z?ot-BgqU5ptPU%~A(Rf_ynL{%_6M4p@VAW{aad&q{@?gXsB*jV-9vtR9B7NW5P+5B zORC33^YstTM?v31wLieB?Mt?SRS?CC|2+F5?zsCQ#A0!Hyl!T1Bjgj!VPy{ogGlYO z4?iv3=_P8cw^o_pE(EcMra}Z~?}vd;PMBle80ED1YPH zKMgVHuCbf&l4{>q?L!KB_Wcdwjea?-8op#Z$QHWfO@H8_Cve|G{|k@D%B3!kOvFgY zr|7xFm!Ew$q8D7Cv%o=#cd@#s9&KsH4eQpytDd{EsAQ_#(&8b%DIUl7moI}yeYVRc zO~TQYRr#E-W=9c&Z=wBMyydU94Z)l)2b-(uNvFXIV~{Z!hvf)CG2h;b$LZ9u;u1Ul z`IEnfM;?D#Z?%iS(_5X8PeHuLl_ehj#V-*1_P1e#Li!9{Ec0vEuEFn=n9K8Qt+>>? z9WOEQzg)cVo*jkddwPiXj^vo_i&bPNqMsWP?*j?;W)_7d4 z&i`=xU3lT8*LAoV&&LVb38bERQ4vD7-Hzmh@!Cq45V5cl`3sgV!ID5gTj}ZCm=Y{u zSC2Q>*W*UjzF#{YCs&U`q_8~WKMhGFw%mR$nd&4biDa(J`Yy`m1)3!Cf7Bj_;?eOZ ziun$%((JAEzub90PCNTTy!Flk6e}S&O|HiY*-<3EJUL?q0{7g5RLxj@UM}0}tHWWO zv*;tNRF=BGUq1F6TI=gjI@yB%!-jgyd+!}Q-L5Ql(@}VJoj)DXoIlzcl(sc#0tI~s z?jO~@tXhNJaqD0eJ|R1Z!(r-t>JR+$Nt}54`S|ZkZ_*+sWFV7~vb>`wpN!D&e=lC7 zKq7%+rxVXA;X7Yh<{@RFJB{3yKbd6Z%-?O8xMh7kj$ZH{UhnMG4s{gv?IN1A*6u#h zs&j4a$_`^LOFoCBT4fr)flfY#)ED>S;YXjs-|u|{UXNR+W+jB|tjeXeB_8?lj}bfm zcrENQTZ<)r^SX6-pt(sWj>$KA7bn-OU4xsIxL-YGD)uccMYmeWP~5kRl*_)yZgLLa zML{RKgk*4g`L(xj(=B%(sjLR(S0`kE$a%Tfhu|M?LTcJHeO~TR)_F?VVizf^e4!He z(i}N2A1IQ4Uh>9&Z`o2z@Otsv>ARuEuF+0m&w+&*yC4UR2-S*|aBpa6!o`hY2l%h3@}=+}i36C$Nwr$O_J%(Jk2-M0;pJE7>$0U@re`H&;Ciks@z@u> zfXKxc!-&On8C3CHe|}Rtu3EhYZnaK!7vN%9EOP0G7dtlTFts=4(9P6w>PRK4G)@CS#@FkZZanebf>z9iqe(U=6y68zZ$CnQk z4%H`e#(qQnI^3)JRv_*T;@U4@G@g;NDWU ztaMBKc;Us@asEY@p}oD+Drrgx*)`<6e9RaG@444juA zUS3>;v=VU7<1pU_o~e7fTfM!a%}f#ACg!y#wF+ z&QI|E2a8c$>}P#)LIz39BNttS@Re5r(I|{mN}u=syQU(Y|zIUWN2Vd57$bz$d9+)UVv+F_@j1&lX9no4BmoK|NrL2<2DzH@?QVFIQG~=*An7*k>PoUM^OPBu2SxWgTAb=%n-8kgTW-6sdl`eZzXJRu=m&rcBj2 z;!`wOGTZILZ4q{KbjU-&R9ln&me}%p?tc{bKlr$|$_e?T@gkF8Dw#qenLtuKibI#A z0Cu_@DDwMJT2idXk%Tsu6#3xu`E=N0aZwQ*P6y)gIHpV_x2i|6$~W5e%*ha57S%$@B&MjbDPHih1)rz+;B~dNii!#fdZ7rq z6i<&A$xN{ksZ3X>+(V%)eIOjNC^sse708(z0Z zYgZ~ILZ7mXY}@kL+7kcNdt3g!zYuTzT$*H0vqjsG=hiRRB}Qdi+uX1bW_>*z%a*}h zy$VKC6HEySQ(u#oa7z*WArN~hu97(95QHwj9EOxfOIpQLw@<;3Xfk(fPm zI`*0~OIgnoFk(cddf%l*TCDI?Dybjk{>=V_LTVtZ$yzM`v{?Tl{7TTx_I8+cb#Q*L z5RQc(z+AsxXK71IwnNw89WjZRN5A$p-FkN%50FJj@#7QGU2Ll!7X=pFC92iHq4!LewO7U2Pjy!4%F;j6Af{D1?r zu*-3TY^qI8{q=~hdcj$pQ_@I4JT|GkSnYEoRyE9DPyOq$3$ji385?yoSMz~&l5zgnH zgJaz~Yn$Ku2}Ouhf2T){Lg>2dkRCl6=}1J!mrL;w@vdH{Ui)OHfIXd+$hrEDCr-kN zBS-2aG?T_%)zqB0mRP(7t!3WCHzidUF zKCg9g@s!=`In`v#{RiX6<4e_}^TAT5py#uoOaAo@8ymG{9twpi=6PUHMwHbj;f9CL zI|wHpe-vuRjzKgQMNA1eQE_?BMW2I#5(`sgb6rx)M)iQr|%;QdcF#}Smx`MnE&Fc>(Sj4;5K`XPB!Ai6ADLA zJ#qw2J?U5+edM7^Uz|$VV@v=vJX<6Btlf~%yF>z}c;2hGf2)N2+4gp3CCfol%2to1 z(ztBGM0{c7NCa}%QqKxO=W=$i4GJaR8yiH-Z$M8VNHNbrf}t=*R#o83XPtt>4?9Q; zM>rZqQcX7DFpEwM=QfFi%WW2S^&nT{Gv(!2p{(_ls)>azvd-mJZeXepEA!<{or0e0h@stM&3Wr?45@^SjeMEoA)OuW}( zCt0TN_H{X^tQ_+yD&bNuo^RizL)BXByzM0Wk!vUw+kU-fHTq~CA z57J6gJZjS^T=}ctW681=3{A@kgD(_1wq1GAo4~2!*34D=o(Voc|RZeDFTXI*;fSswDrB zG_bAl7;ad*7O!-6>ennanVm=o+rj1KxO&P|{W_T&qE122bwSsKR*L-i?<@0h-z=hnM025zmd$3I(}b@CbJc+axMH>_~B zFO90kB@-s(N{l)MJ=XX}Qdxg?J1>guLPd$z6gm5`4eQ;q-kg9|Wo`V{q=LQX71 z#NDQWzi({7T@4Lzs^>1AvuD-fOSa#CF=;Z6uByrfQKz8ivY?9-oGk3G_|?r=ym&c$ zUN0wuY*^jP)_N#{%8D{v_Wv%#9+7wmgX|ItdM5DueR%7g1$g4A z=d`es;U%3)>vQrie&KU?utN&^psg6y zjV%`WN#kw4~HS8npo!Vba&$4ZLK=t46gzOeV35!U8!^Zz{5|V zI}n7^$*rFuzp>R_9EwD6;rZv_x?lek4%1}qaguSmm9@TqX(^uAYcI?!D&pDtaLm7u zP(u^ZG6#Tu9P7Y0SIte}wlxSfVX;GPH;XyT{|gE!SU(F6~nwuC}NI@Ss4u=ELNDNOs{X9}Bmi8NJ zLg6rG>^22|yZyJQtr?9-ZwXC8hH_yKD=q1Twd3)-X}f8WPjU->Nc&&%1U$EC6PAWT zEVx5K9~fetzxD11Sh92_9pQ$UU@(ZI4?hII`Q>GLr$izP2pLYqLGIx4GW>bQ40ucj zMSX|`t2>dvqs`6w7?hSe1%0Qj^PZjnp8U`AIvk1>xrS;vZpD_Lea1<+_tu;7EmSYEf9#aD)E z!c;S6K#BP`zIr;oa`ve@?w^osLsF57nBO{MCaOIi#F))&NRTi#2`l_pOY`Oh7NMZ; zkgz1xz6Z{qE5&x3=6#hUW-aXX$Tt zbz+H{AKaWDDCj$a$L+z2Rco70ArjW->V$&6U7SuQnp@knce^|%nI#g5 zVz+6N@y)NDg=8Yh-4K#{3(`~)2bGoMn>Dq%f*VJ0W)Tr@h(`6+yOZ1Q1u5t|bdQOc zxLi)GSg{&SO|1-7%P^9cy`;DZ-?{KSowO&xOk5;KkvR1;s;hBCMTO2NN14eShTUFK zR=Vtvu&-&MZ+oD~cDs`Pci#O_JI2vk%-{-3^=&K`!&zsXh$)lCv%m_;{ROIO->w~p zF>Vi{oI5gVok@=OWx*g;N}?HMu@`!8Q_#18Sm(_xEm*N?4IJEV&nVGo4Eydq2ge+B z7-P*zUJzH-Vy)M!Q^d+yIwd``0wy~lZ+CaWp{y^1{Xw?I>2RWM#agtqw9^?bgGeZ# z&+EaLPCZ`tGwYa>d?Bnv=Gdwc*t57;ZwHgivRGN^BJ7R=KTD^eZ-o`}oW_TXmLVFC z!=(3QX8Ktsy+{;?9K1hfPM?ZsEXGbDd4p`bdmJWCuO1Ck%_)@h3>sjne^-QpXjXHG zi-J!2BPBVdlGnO*>uH^5kXRytveIH4_qoHgy3s6-qzlMjg8IvImrwObp!4-XEr8H9IwyO3CTMvUCJ)2IpnCyN-Z>u zO&^?vQg7R0@ud28bvUH`6$)9>>G9PMu@WWbeD#`obaw|>_dHYmtQSrujyUWf{TPih ztuDz=Bo*udrRA9H^C`gz>yn>^BE0?Y_pKK?NqfFc+yCDGxgcBTE@gQ;?4RaW^Ms_> zEmzGeRn}A+?eU=6?M9`VvmJ(sk?P;XccGA%O%(M0gHVU7rO?>avN?Aq;ULY$4XX`sq1UI!=WYao~H?@`cim1vKue7v! zE}7fpZ!J+xLdfK2ipY-jcv0qZ!l&Mo_eZ(hC{8<_>!}=!sd0ah@Z{AF| zd&unnZOXt(sl|U%wN%u4o(RE=cdN&4n6WPPZ&5@?&qLBvrT#rZK`(T~qdgjnqN%x6 zJI9d>OmON(H`S~nXX(Mc40;EoNv-SGJwYoR&Ddl@NqWe#T~7J#Bp~_4jRV^81bcr|I%FDa1>6L()51dL4y$nodFQ-x=!0XjWq0-4oEF zLo#8VCs9^ff(aAG>e94?`XI@7*kVW}oo(ZD^~fNhQ8w`Noy729=P8)h`y|4O+r0 z0<4;;n-XC4HApIfPKLF0EZ-+03bD&m_Roa5mGJhr#<61}YdQxYO|qDX!4YlmXX(>I zFL(>RCm2K|65+XShDd2aFU5$;N+yya`E8xsb4ahvJKbOOG2u;b7GGP0(uOUD_IDcT z&DN)>#1yHp65p_8$;P_jP(rIkIM%gUbhSmCRDv6qN3nL}))urD;NGq1UePtSvUZKF zEZ;e3D*hf^KYoHEjucAP_<*9D~cr?cj_Ey3+8fs&ZZMCY7R( z0x1w{U0I!uPy;-z^DUuG^w?H#KnZd{Tf5fTa9mr#mgQ^qo3V_o#MkU+_3nV>Yd9pd zB>yM)*rtBF7lk*!&_dt5nzkN0ySkOQC)I1lpl4)aQxjEXg(c|mc!C{53S@$DY75%4 z;VJ8kdAswAnur`e_SXRIHOm>Ua18~Tq1!Okx!Inc)AC~HLN;Ryrj)~vw9{L#izh1AR$?QeYiWv2<_4VV-}X%%h(w~y z#zzWB&y}D@TP;E7ffJ#iXWBxy?z58OVtQa_bY3&GphuOUGxaMeBxAu5X|j^La0f7R z-Ry8v&`Do$0gu-Mx0~VlnL=vJOKDoEH*eBHCj~LNm^x#dqh}q(Jkx^it}19*3{>cfet?(psjFeEB_r0M^xSfO9a7Ny zkxZsgQC^11stOhk$*7g?MBR!tXlicJ&k4z6mC>a{U7OzHbeqw2lQ=2(k@StSJ=|%b zb4;i~;dD4LzILn@cUq5`!R2zIwS5y7DJz|VPV#zAQP#O5um-Nq#k9^d+R;g*T_s3) zMq0V@3pA2|g1)t&D}kx48G}?h35J(s(rz*E@=Nm(42HC$93gpWof{@RZEwMdcYZ=a z7BU!uo^n?p{)Od%E5#0l}q ze?N~%B*rwbB;OQ9iHir`#^+&%8`MneW|w5JujM!+Q8Lxu1)-o58_O}S6tiYbRf0o7 z&m>Y4ebJISyfObh_7F2y}$Ybcd((qA6Z6hvjK0=-U?=$umB8ui1JFo>pV# zkz4fAQh69G0Jp3!~gb``wR(s|<97x5qce`x48_9UV_Xq`jTf;<+ zlen_d_uprZw$!817&|57g6=>t6vppv{1Y~9>eT)VgyfcW?uBp7{cyFtr3Q2nJ0#;+ z7I|#cp*n$2p*zG;&_4kQsTwtM1P(iNfBl@hz?sMGa$;j+3x55(+YpJxw8#^Z6Jo9e z+}rRpyc?cWV(tgM+J}H-3`yT;#H!{hE$7iF1^tt5P#G_&6b?Q308~_z(K&92@OeF0 zv1%=Td;K5vHDDGwk_%$)X?_L%m48(;s#uCVq8plyPasjbH&WhF1@GNXLEix+;mO1a zwK!nExrj#N%o$v{>q1ELpY!Kl<5k5D7ku~7#jQ9Kc#bxuLwQ6z+I`t&I{DGt4v)u!^&1;;@sF=T zYuhF!o*`K!=Dzh$plIpMfWu46d?--@o_3cZI_7AGs!`DU?#Zp1jEp3<+Pl#)(aJJE z<)mX#RaK!ab~1FhT`qKWb>qVC{T%b({eW@mWZ134w8Mv@xNp!eRHeUFCS7p5)hTewU{V=;^$SA%1Z{w!kgIOEcXdXSo?-g3X@+FNkf zz5haSaS^Wp8F~vq$Cam1`oS;Y+4wX=(}qRNlSuhSBU1YXTg^P970B`V%L zUq8|^AD-)gbWDr6c(xaMnpXb$G`t zcTFf9!DkQIAB&bO!+)N6NuTGEAzPax6bfTZ^+UM9j*28hW6t4& zBe)JFAN>Yqw2fQr!+J52NV|#=ntZO#mv1O>XH)?NeV0XDyxz|_^JE=TwP8auw|z5w z0d&SVhXW^m{wSPr>Iv{F3tgfO0>Ka*a<;DiZPAMLFbxBW2v0lXfna zJ;g;{eD2UaQC?QWDtTl;OUSg#4^Q)}C|Y`p?Ji53XV^)E5g)NH0<$i)ec-b4-Y!?I ztD>TUf}U%FE?rh!?8DvnJ&Fha`J^uYNp>Bvyho0x#Mi!Zx<1;7$K!~_5<0|JL|k@9 z-u-Abmab~hU+;8$+yx}6zJ5~e2+TWZcWuEZ>FYptA*=R9ECGZI{{v= zcXI+C30HHN2I^Kf;_VMtDyu!Ft?^G^aHSDf7W$kS<8i=V)3rlmim~ftJ3=n?%Zr!X z2uIHfz(Mcztg@Wa2?S>U5V5L#VI;HK5sreM%YrUQY;;c`h|7O@BkI>Tz~f;ivmIJ= zE2~)?4Cn4S8|Q!ROiZ0T0l`p6Z=0K@p-ZQ>cl6+m_m-ofr9+GOK;q6NRj*j+`|Lgm z`|L3pv3Qb6X^1W4ewfiF6feFJ&h9!oyk#SaFv3&5g7Em0>~r(1hJv2!f-Z=NudCmP zpI&jD-Xa#Ol588XysIk9@%68sfx`|tKv~fV#Fd3Ed+IKiLvOFY_t6?GS+QQ9UpfcL zH$fHGTL?ejU=)(}4@jaclJqMtm z=YyaNKA#V-zxfWXzxglPL5`ws4=yFtvZXGLEXN)*59geD5{irb{kGH%-Crx#G~(S4 zS0NCL!0pbQ6Ovf$iDVM9rq<$sxzqF+ySyfOKM?iSx~K6)_*ea1S$jcxt!Fbzz>}rB zp=XbuAZ@yHC4meDJzoS}5XZQupL+?n{pntKy`DmzJGBKIi9|7H_Zj%+d1qqU)JX`5 zm%4gi{#~}vH+2Q@`n$`~(7b8DTj~S0(o~Db6R55#!=VSvL}ghqVsYl^&pT^eiME;O zfq&&aa5ulGL(#0NXxyF2T2gT&ePhtG=aon~i4=E7yY+RQ-OY@&b6Y)!#60b;usqtmRoU#(PC+jOK^G&_@AKpC z`yRnV|9Ud-Gs#&w?&C?QVkP1}JBFOsFE=h@QeVTKJugS1 zXo8&yE~lWN=bNDSx77dqxBKwFPdmP-)W3hEEZt`3_cQ0x}J1q9>@pC2Sla*zj zv$Yv0==m(@f(ZHj4?lr>?|W1|&%JXzF{&k$OcJwv?x;iY<+DykMOmqiAs6(Px)SX* z>s#^G2X*KUM0P0gOU}+clCUP8M3LWvDHBFx#*`YAm-;u0J{e>yTnV*!Ih)ZAc$!{> zr{Nixkygt?T~|D#i08UxuD99eT!CcqcwJ2`hoPY7yPyk_LGJOV{tth>^FjUG$bqfr zl-^PgWBRlyxbU0jV8(7!l!!<5S-Gjb)LrQ44&bc?E3j^3o6eYL7=>3Xp;ySuON+4E zxZB>+0`3T`hCb*qJ?x--A{9f*U2ciFZ!`?` zI>(`)7lNQ`OWp6s)BpD(ZvE3eI;1UI#ZStUmX;Rd>@&WIUUf@j;LB9Vx;zzzL5 zo#^TbVg7;@ShJy3$7zeWlR+|2de7X|g39l8qq4jhHKVIAs;Wc_y}V}_roAXgTZ@9M zU9y<^JUcCC+V*D_k#>X|;Otti#oG~F3p3m-f4OCO8!R_mm|`A4bnGz*PQTE0e#@&x zECs#L1YM9+r?Ge(S6*`ymakZ&w`Yfna5xH|&x3Q$_#(b={89R>SCYL5k~dw%OplGKBD?a}l0!N-h*yp`cUHcO1#}?sBAx7<=+-`I}%@71BBJE$|d88pVT0!dFhaoi{zanSBs9Hdk$M3;+B`VY-y*Q#oHBg$;f3W>q)$h#W_imdg#Z5vZ7Ma zz6F9j$nP|r{luP-Pg$LysTW<*SX>LKQ*U9f2VBM56jaH5t;KmzK7ec~(K(F_aPXueNcqO1XU{8;be390 zakvPipy%ggtyZmp5RRO8FlO#H1(#oaBih?Lcd4ejWN#BMdf8Gx@$*M&r;}JTrbIkw zZ=ai3RJR@r7Og=%X>F-f#ItwNS6t*p?dXbrg=5lix4n(cXV;9wCfQ272eF#ZAu#>h z)(%iQPBEvT=ReVC6r)B~;m?2gHLktkPniGC`#Q{RhwmFT*~>ZjXAayO7kvF}RF4{g zKrn=;df%Z|4U&d+V^cfcc&`raojuwSPZkqAAJ1+QiIfuaDjnA@{tU8xF5z9y?q$r3 zMzWW5N{6KdXJ3qH^$~-&TSO@6xk7K2u`7T6eLViubNJKW?$^$8+uMQ{zXwUfI&pk0 z&Oh%A?7QzAM8Z)W=PY6_R(DWY>#r|Zi4|*`w0O&wx@Q=?>d8Rn#I)XKpEz!0pOc#5 zgsbx-t9lr>!AVw5WD%tNH99AI%3YzvyoXjf1-)>QE%?tLa|909cQ0IV^-b8&(6mKu zbvbtzQ9kYD<8jItkA=hO)E2&cpPYkB8rCH%HsZZSt8~g%mlAdlo zs3=7$A&Wf|Mk)$d`}>v!L`ZfmLDirQAu{oFgeIM>ggk6#exsOE&ta+p5X&pLv+`PR|Syrr%QPR#L_*UN!-`WQOR|$B2 zPO^e{0MVLb5!8-v5i5~G;li*r@+pg7HJMAyx;VaxkUP}#Iwj!md?;J$NhRzKxShO& zc_rw{6soIBQBhW8=YBWzS){vdzLg@CVorwF2GrqbX;%fh_qqnj;t4R4VRlQFKsCq_ z#HmfT)E6$R*W0in<|IFvP&-mD-jdon_ru-tCLEy+bcQ3tOxnu$Ht}rLPFF%6%H{gy z`Kc4^f?ToIQ>Z8{!dWMN2K&yLVx6tBJZIirC%cAhweK-wJf=?`ga3PFAzItJ=>V56f}WDt^GcNX zJxC-hXE;Zs!HPX+H8L_RMtM>TcVN~fh>iX%jCfBz)Gnu>lkBqOBuwq3WA?{}#&-Po zjYZmGr(;|W&zow}lxsS%c9b^6we4d%;M(*NjA(~_eon|xTIE4R#-D=Flyj~8{&7hX z!(F0mrqC21ul9%}*1UQ|InF)hFzhvZqD~RYZTMWZ&eN!>D8|UD5}hSZJHsU-O3)Xe zFV!m{LnGwz(&^|tbi@JG?PL%udm1YuK=pE&F&pmq8 zryhF%zIfC=I&+*jSdr{Sw%bRKC_{N^k>%ZPIF*oh!zGo~3@*q>ri47MCd9N(4AXJQ zeTY}gu}hqi9pJyVRvET-Wkttj?s(J4ym?QURI2APC67v z9=wMYLYm|`ShgVsxoH>}H+F<}iW3W6taDe_5;%hO_I5iVgE{DuTFfQydgmecBR1+Y zc2zVU>wE&zL6_N@Bsy}WtyNh?wxoXDQRb?4n`#GfeC^yaMlAI;^(}byou%5D&dII# z?6A(&BCDv#i_agvH+)_f#6Xwr&*G&w!`=Kc;4J1uO9n~E6FRR!Wc4X_+Md=5ZTj!Vtz1ztyuI9C4TN1a{VIK~INw(Kj``n>>V(jQjBoYavl|}B| z_;2`E+yN*{osfYGc~tvj1gD*k=-8vJZFPpVZjD&q-%;(2!T4bm^uZJK{>VP%FI4-1 zXcx!q_L0h7ojpOkJbwvV+Pjs|Q`m=!cp{0BRi!xgus!wpc`9vf9u+UX-ikk`pp$`+ z3?uC>M{xQ@h>h6ac66hV+sJms?^XLH2G2K(f}UYP-`7KqASK|=o|PzB^c#D-ow)?aj-uvqHD(eOdnh6Q z7Lxv2hMx5y&THR8=Ax%4=tC^%{gGJrYgGFdm{{jyh{v_*FtMnv9`Aj$8i{y{mEAHx zDwWnb0**a=E-K54v~yhA;e&78zu;T*fEt-Tb`trt>09Y|TzjlXYED3S^4UmB$Rm@% zNKE)&RQnmM81W&4f<7q+I0)PdN|K(erFaw}Q?$u~M~5RC{tb zKg4hgIxSS-DZ2?HRl8QT^T6cUh{lw_l=yMt(fjIzCm$?ci;tGoTZKgEL^p8KR(<;k zH6!74n24nkI`-V%`4Jp}HLQD1wr!1z<*iz5^x+6i`Kl81G9~2ULQMuEZu)nr_7l}w zI6i2h?+B->cAaYDIaI88KPlDP(G|cO@7G~tV>_JcwMnym=Zvdl3dO}<96xU^d|r=E z{hD_AQM~l`a5ufAmf3!G66st0CBjJgMk6%!8;FhAuaK?rdRU>#4`u5Iq=lZ{JZv|i z#uoQ8QJ}HxvhHi;BA)89QxmEA0>a}@(qlxIFfA-=T&A1{Z4uwVK|w(uOg5@^w%tUv z&QtAIs*Nec&Je3IrrLy>5tuTeTAu?iTe%VMFIj^~B&NgRsO{UBq0-_EXJW$GQF`&C z_s0weTsD zcD-sR7y5t_D?Oe}VR2nOK3rO_&*3TVeI$}ejHoEZ@rT>_k3`hd21*xQ3ul*D=oIvV z6Y+Es@v8k1nsP3ZMdPgc<0z<1ZjY(JbP=;FfK9vvActl}u~zcBxk%i^laiwaNky>2$0GvZdap+7+-u%Mu(Dw9s>jnX28W+OdT^ z(8McVWW2ti4eu{ngVs&maF`|>j=~8hNUI5368Ic{_}(bjvFAzc4Ch_@FZkEoZ`VEN zE@R$`xWk9Y*v}y{?u$sdE3}7t0p(PGMzu?kg%!$Zp_83qscOgB&&34$u4+GrmEfje z=&i{f`EJgQ0yd0S@>7B8>I>iSll$K09cPId7_3N@ogpsdud=JDRT=K;7ky=&J! zrC{EW#G1_?>pZRh#zXdLfQWa7tU@X;-n{N*57N`fIGLZ6fZ4!Ar1`yDTWwVL%JLu#tg(xc%H`-wZ#7Ra~Yp z=0PQNB5p#>=t_(mTcw3O9F3uFbt6`;ZNjF`fY!U-ZTp-w*p!%;l=v`abcK}}PN(ez zTrF=}j!}BMo#d2&ry@F|U%X-tB4dt0LOrHUNxd4;Rm}>(3YX$0*V;x&n1g~AdZAnB zpTvQ%Qqmq+$em!FvpW!p#ZbSY6-!q&=rpvVhRz)Fo{y$K92HyR4A1!b$WpU$u+vM?wpo?8J?8ko~;aw%jkV zE%|~$QqD?~Y;+UHjl$#!)!Kp=Z~Bc*o3L_S6B?Q~AsmV8t$SGjWFwM&0y*Sv3;D6PwzA}v$V-(oSpc+m z_MpC@9gWQ$=;#h;p%r?q{Xr9Rse7pqAs3zg+1R_=DE5B@a|!MpBZJJl=G=nbxJ zr>re=v8o+@B>lBYv}Y*s-Vcf5DN4+J`pnxB?_{C&zr=$}T=+8_FtpG~_VP@>n^ZE@ zNyfSh3jJDZOFpS4Z`*gNqO4fAxw9uKk+(!#{=KUwtex&QHg7^(dpCmNs7?T5+DUMB z)~Zh=Q>YzVrGJvWudx{xgIEruej%xsdOt&IaHKcS%KH*(|tkKF{xHUB%+rm}1LEa+6K z&qKMS$g7*RZST+)y(|V|%CZgw!f5a4K~r0&e(VWGb*}of`WdFf+5y;F@FmOb#F`QM z=ZKz*SF|&*ShdyKW14(yFU1mGE!c5=hOP76n_i^7Bb0?b35n9_O1!5c;Tx^vzIBAa zW{bNw^IF=<#P+eDT~MrqRjKSTjw=c}$v+1a3WjwoM5IR%|w?2zk|4a6Yxh-yHxEW@&1CB@| z9N`AlTGZ<{7_rVir>tIUQ(OFYGfexnx#jzP%aB1D3fs_$bh=N3`@h~S%%-m8o;I9% z7eW%{NWSuvyAmmXHIlxuNGS_D?Jh^sRc396>)3l)OD3(tByyg;Ii8zG5j#jdtdQ0R z!Q8>a&TyZhQ_#t_7;lUFnVeZOx({GKno0;AmcaE2wPD4p_X@m(PDvhcIsY}%5g-xO zTgv*iYGxv+UI*=5;eCq-Bi^OHKDC|SE@#greR<^Ne@?FwTc6cLlwC!Z_?jLi(q5!Y zpLXOE$39EEojOTQ@8|6k`Cfre+3#@}4dS4A&=&D}b_WHWf=+e@F`>?bwLL$E-I0w8 zf=pPnnf%#gCz0)Q*+#z$1y(4Xg1+;UH6fpbOdxlvHrAfmr-4p3S;Drq-E-k*lO5nO z`(88bd+$z)IiaAF!A3l1Pq8N&7c38YhQScBYxG8>xa@oD6#HHygn~|n1aUdOT{SyN zIc9^2DhSz8p0V#4!@h@Zx9=%JDClI^k?d2)*%Oq+Wc@!dl_Vj(blUfX$G%67v+o%~ z1_e6Bk)dJ^EYJCQsyXb&Lt2vabLdvDdtLqIyjx=MUQ+Ec)fTd=@*jG+Q_u?$DFG|h zoFw$7nB770h=9Ftm1>A#b_WH0DB+en84hHk`;}@XwuK{g-{-SiNRBe!9>az`rhi2- zClqwT36T>b;ewP93OdOPWXn>LX!z~P@^IA_uv^G5v%nsUhCMcaZjV($DCmUjATO!5 zw>^={0hQExkFi@whKbo@t->CAd)s4?5DGdWg8;*RNG4edhFua0Iw8XX6LXMVA&D&$ zBh3O6BM}NZA;ZqLQfIDWw~(z^jlR^yPB3*bp`a6z^+<7xso1i(1?xo@9&*k9j=thT zQ^4Xvgn~{;&LZU}zhRGeX=kgpf!&g~Y_zX&@Adw=Jq8G&pc9g37TW`=jk7Jb393EB zZpnEbwy&jJ(^~r)b4P|y&Qy__76OND$+xguvYuA^TJ_chIK#e{giz24A-bT3 zoV8cmA~8+1C)q7o!&CM(k!v%`zE&xA3n3JALOzX^s-0*HhOM-hYsoM@1QSo z;S2UPA%sxS2^laF-!3uBy_v%#{{01Z%b@dL^yLlpgn~{88CrI>bZBo0*;=rKEFlzhLP%D!QMI$}dt2JqRBN=`*ZaMHw*9q) ykUYUir_;mw0XjN5cohgCgycLG6&1t!`~N?)2wkwq`RJqo0000 - - - Chart.js | HTML5 Charts for your website. - - - - - - - - - - - - -