vendor/assets/javascripts/_chart.js in active_frontend-12.2.0 vs vendor/assets/javascripts/_chart.js in active_frontend-12.3.0

- old
+ new

@@ -12,12 +12,27 @@ 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; + var computeDimension = function(element,dimension) + { + if (element['offset'+dimension]) + { + return element['offset'+dimension]; + } + else + { + return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); + } + }; + + var width = this.width = computeDimension(context.canvas,'Width') || context.canvas.width; + var height = this.height = computeDimension(context.canvas,'Height') || context.canvas.height; + + width = this.width = context.canvas.width; + height = this.height = context.canvas.height; this.aspectRatio = this.width / this.height; //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. helpers.retinaScale(this); return this; @@ -65,11 +80,11 @@ // 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: "'Gotham', 'Gotham Round', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + scaleFontFamily: "'Gotham Round', 'Gotham', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", // Number - Scale label font size in pixels scaleFontSize: 11, // String - Scale label font weight style @@ -85,18 +100,21 @@ maintainAspectRatio: true, // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove showTooltips: true, + // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function + customTooltips: false, + // Array - Array of string names to attach tooltip events tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], // String - Tooltip background colour tooltipFillColor: "rgba(16,18,25,1)", // String - Tooltip label font declaration for the scale label - tooltipFontFamily: "'Gotham', 'Gotham Round', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + tooltipFontFamily: "'Gotham Round', 'Gotham', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", // Number - Tooltip label font size in pixels tooltipFontSize: 11, // String - Tooltip font weight style @@ -104,21 +122,24 @@ // String - Tooltip label font colour tooltipFontColor: "rgba(255,255,255,1)", // String - Tooltip title font declaration for the scale label - tooltipTitleFontFamily: "'Gotham', 'Gotham Round', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + tooltipTitleFontFamily: "'Gotham Round', 'Gotham', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", // Number - Tooltip title font size in pixels tooltipTitleFontSize: 11, // String - Tooltip title font weight style tooltipTitleFontStyle: "bold", // String - Tooltip title font colour tooltipTitleFontColor: "rgba(255,255,255,1)", + // String - Tooltip title template + tooltipTitleTemplate: "<%= label %>", + // Number - pixel width of padding around tooltip text tooltipYPadding: 8, // Number - pixel width of padding around tooltip text tooltipXPadding: 10, @@ -139,10 +160,42 @@ multiTooltipTemplate: "<%= value %>", // String - Colour behind the legend colour block multiTooltipKeyBackground: 'rgba(255,255,255,1)', + // Array - A list of colors to use as the defaults + segmentColorDefault: [ + "rgba(151,212,19,1)", + "rgba(75,173,8,1)", + "rgba(59,187,178,1)", + "rgba(0,102,255,1)", + "rgba(86,21,237,1)", + "rgba(115,24,242,1)", + "rgba(255,0,102,1)", + "rgba(240,35,17,1)", + "rgba(255,102,0,1)", + "rgba(255,209,0,1)", + "rgba(35,40,55,1)", + "rgba(106,122,138,1)" + ], + + // Array - A list of highlight colors to use as the defaults + segmentHighlightColorDefaults: [ + "rgba(151,212,19,0.1)", + "rgba(75,173,8,0.1)", + "rgba(59,187,178,0.1)", + "rgba(0,102,255,0.1)", + "rgba(86,21,237,0.1)", + "rgba(115,24,242,0.1)", + "rgba(255,0,102,0.1)", + "rgba(240,35,17,0.1)", + "rgba(255,102,0,0.1)", + "rgba(255,209,0,0.1)", + "rgba(35,40,55,0.1)", + "rgba(106,122,138,0.1)" + ], + // Function - Will fire on animation progression. onAnimationProgress: function(){}, // Function - Will fire on animation completion. onAnimationComplete: function(){} @@ -175,18 +228,22 @@ } }, clone = helpers.clone = function(obj){ var objClone = {}; each(obj,function(value,key){ - if (obj.hasOwnProperty(key)) objClone[key] = value; + if (obj.hasOwnProperty(key)){ + objClone[key] = value; + } }); return objClone; }, extend = helpers.extend = function(base){ each(Array.prototype.slice.call(arguments,1), function(extensionObject) { each(extensionObject,function(value,key){ - if (extensionObject.hasOwnProperty(key)) base[key] = value; + if (extensionObject.hasOwnProperty(key)){ + base[key] = value; + } }); }); return base; }, merge = helpers.merge = function(base,master){ @@ -204,10 +261,45 @@ if (arrayToSearch[i] === item) return i; } return -1; } }, + where = helpers.where = function(collection, filterCallback){ + var filtered = []; + + helpers.each(collection, function(item){ + if (filterCallback(item)){ + filtered.push(item); + } + }); + + return filtered; + }, + findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex){ + // Default to start of the array + if (!startIndex){ + startIndex = -1; + } + for (var i = startIndex + 1; i < arrayToSearch.length; i++) { + var currentItem = arrayToSearch[i]; + if (filterCallback(currentItem)){ + return currentItem; + } + } + }, + findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex){ + // Default to end of the array + if (!startIndex){ + startIndex = arrayToSearch.length; + } + for (var i = startIndex - 1; i >= 0; i--) { + var currentItem = arrayToSearch[i]; + if (filterCallback(currentItem)){ + return currentItem; + } + } + }, inherits = helpers.inherits = function(extensions){ //Basic javascript inheritance based on the model created in Backbone.js var parent = this; var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; @@ -230,13 +322,13 @@ return "chart-" + id++; }; })(), warn = helpers.warn = function(str){ //Method for warning of errors - if (window.console && typeof window.console.warn == "function") console.warn(str); + if (window.console && typeof window.console.warn === "function") console.warn(str); }, - amd = helpers.amd = (typeof root.define == 'function' && root.define.amd), + amd = helpers.amd = (typeof define === 'function' && define.amd), //-- Math methods isNumber = helpers.isNumber = function(n){ return !isNaN(parseFloat(n)) && isFinite(n); }, max = helpers.max = function(array){ @@ -258,11 +350,24 @@ } return valueToCap; }, getDecimalPlaces = helpers.getDecimalPlaces = function(num){ if (num%1!==0 && isNumber(num)){ - return num.toString().split(".")[1].length; + var s = num.toString(); + if(s.indexOf("e-") < 0){ + // no exponent, e.g. 0.01 + return s.split(".")[1].length; + } + else if(s.indexOf(".") < 0) { + // no decimal point, e.g. 1e-9 + return parseInt(s.split("e-")[1]); + } + else { + // exponent and decimal point, e.g. 1.23e-9 + var parts = s.split(".")[1].split("e-"); + return parts[0].length + parseInt(parts[1]); + } } else { return 0; } }, @@ -317,14 +422,19 @@ //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); + // Filter out null values since these would min() to zero + var values = []; + each(valuesArray, function( v ){ + v == null || values.push( v ); + }); + var minValue = min(values), + maxValue = max(values); - // We need some degree of seperation here to calculate the scales if all the values are the same + // We need some degree of separation here to calculate the scales if all the values are the same // Adding/minusing 0.5 will give us a range of 1. if (maxValue === minValue){ maxValue += 0.5; // 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){ @@ -393,13 +503,14 @@ /* 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){ + // If templateString is function rather than string-template - call the function for valuesObject - if(templateString instanceof Function) - { + + if(templateString instanceof Function){ return templateString(valuesObject); } var cache = {}; function tmpl(str, data){ @@ -434,11 +545,11 @@ return tmpl(templateString,valuesObject); }, /* jshint ignore:end */ generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ var labelsArray = new Array(numberOfSteps); - if (labelTemplateString){ + if (templateString){ each(labelsArray,function(val,index){ labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); }); } return labelsArray; @@ -455,41 +566,49 @@ }, easeOutQuad: function (t) { return -1 * t * (t - 2); }, easeInOutQuad: function (t) { - if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; + if ((t /= 1 / 2) < 1){ + return 1 / 2 * t * t; + } return -1 / 2 * ((--t) * (t - 2) - 1); }, easeInCubic: function (t) { 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; + 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; + 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; + 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; }, @@ -504,64 +623,99 @@ }, 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)); + if (t === 0){ + return 0; + } + if (t === 1){ + return 1; + } + if ((t /= 1 / 2) < 1){ + return 1 / 2 * Math.pow(2, 10 * (t - 1)); + } return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); }, easeInCirc: function (t) { - if (t >= 1) return t; + if (t >= 1){ + return t; + } return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); }, easeOutCirc: function (t) { return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); }, easeInOutCirc: function (t) { - if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); + if ((t /= 1 / 2) < 1){ + return -1 / 2 * (Math.sqrt(1 - t * t) - 1); + } return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); }, easeInElastic: function (t) { var s = 1.70158; var p = 0; var a = 1; - if (t === 0) return 0; - if ((t /= 1) == 1) return 1; - if (!p) p = 1 * 0.3; + if (t === 0){ + return 0; + } + if ((t /= 1) == 1){ + return 1; + } + if (!p){ + p = 1 * 0.3; + } if (a < Math.abs(1)) { a = 1; s = p / 4; - } else s = p / (2 * Math.PI) * Math.asin(1 / a); + } else{ + s = p / (2 * Math.PI) * Math.asin(1 / a); + } return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); }, easeOutElastic: function (t) { var s = 1.70158; var p = 0; var a = 1; - if (t === 0) return 0; - if ((t /= 1) == 1) return 1; - if (!p) p = 1 * 0.3; + if (t === 0){ + return 0; + } + if ((t /= 1) == 1){ + return 1; + } + if (!p){ + p = 1 * 0.3; + } if (a < Math.abs(1)) { a = 1; s = p / 4; - } else s = p / (2 * Math.PI) * Math.asin(1 / a); + } else{ + s = p / (2 * Math.PI) * Math.asin(1 / a); + } return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; }, easeInOutElastic: function (t) { var s = 1.70158; var p = 0; var a = 1; - if (t === 0) return 0; - if ((t /= 1 / 2) == 2) return 1; - if (!p) p = 1 * (0.3 * 1.5); + if (t === 0){ + return 0; + } + if ((t /= 1 / 2) == 2){ + return 1; + } + if (!p){ + p = 1 * (0.3 * 1.5); + } if (a < Math.abs(1)) { a = 1; s = p / 4; - } else s = p / (2 * Math.PI) * Math.asin(1 / a); - if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + } else { + s = p / (2 * Math.PI) * Math.asin(1 / a); + } + if (t < 1){ + return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));} return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; }, easeInBack: function (t) { var s = 1.70158; return 1 * (t /= 1) * t * ((s + 1) * t - s); @@ -570,11 +724,13 @@ 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)); + 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); }, @@ -588,11 +744,13 @@ } 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; + 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(){ @@ -691,25 +849,32 @@ each(arrayOfEvents, function(handler,eventName){ removeEvent(chartInstance.chart.canvas, eventName, handler); }); }, getMaximumWidth = helpers.getMaximumWidth = function(domNode){ - var container = domNode.parentNode; + var container = domNode.parentNode, + padding = parseInt(getStyle(container, 'padding-left')) + parseInt(getStyle(container, 'padding-right')); // TODO = check cross browser stuff with this. - return container.clientWidth; + return container.clientWidth - padding; }, getMaximumHeight = helpers.getMaximumHeight = function(domNode){ - var container = domNode.parentNode; + var container = domNode.parentNode, + padding = parseInt(getStyle(container, 'padding-bottom')) + parseInt(getStyle(container, 'padding-top')); // TODO = check cross browser stuff with this. - return container.clientHeight; + return container.clientHeight - padding; }, + getStyle = helpers.getStyle = function (el, property) { + return el.currentStyle ? + el.currentStyle[property] : + document.defaultView.getComputedStyle(el, null).getPropertyValue(property); + }, getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support retinaScale = helpers.retinaScale = function(chart){ 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; @@ -773,21 +938,21 @@ clear(this.chart); return this; }, stop : function(){ // Stops any current animation loop occuring - helpers.cancelAnimFrame.call(root, this.animationFrame); + Chart.animationService.cancelAnimation(this); return this; }, resize : function(callback){ this.stop(); var canvas = this.chart.canvas, newWidth = getMaximumWidth(this.chart.canvas), newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); canvas.width = this.chart.width = newWidth; - canvas.height = this.chart.height = newHeight; + canvas.height = this.chart.height = newHeight; retinaScale(this.chart); if (typeof callback === "function"){ callback.apply(this, Array.prototype.slice.call(arguments, 1)); @@ -797,19 +962,30 @@ 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 - ); + var animation = new Chart.Animation(); + animation.numSteps = this.options.animationSteps; + animation.easing = this.options.animationEasing; + + // render function + animation.render = function(chartInstance, animationObject) { + var easingFunction = helpers.easingEffects[animationObject.easing]; + var stepDecimal = animationObject.currentStep / animationObject.numSteps; + var easeDecimal = easingFunction(stepDecimal); + + chartInstance.draw(easeDecimal, stepDecimal, animationObject.currentStep); + }; + + // user events + animation.onAnimationProgress = this.options.onAnimationProgress; + animation.onAnimationComplete = this.options.onAnimationComplete; + + Chart.animationService.addAnimation(this, animation); } else{ this.draw(); this.options.onAnimationComplete.call(this); } @@ -819,10 +995,25 @@ return template(this.options.legendTemplate,this); }, destroy : function(){ this.clear(); unbindEvents(this, this.events); + var canvas = this.chart.canvas; + + // Reset canvas height/width attributes starts a fresh with the canvas context + canvas.width = this.chart.width; + canvas.height = this.chart.height; + + // < IE9 doesn't support removeProperty + if (canvas.style.removeProperty) { + canvas.style.removeProperty('width'); + canvas.style.removeProperty('height'); + } else { + canvas.style.removeAttribute('width'); + canvas.style.removeAttribute('height'); + } + 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 = []; @@ -848,10 +1039,13 @@ } else{ this.activeElements = ChartElements; } this.draw(); + if(this.options.customTooltips){ + this.options.customTooltips(false); + } 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; @@ -876,11 +1070,11 @@ yMax, xMin, yMin; helpers.each(this.datasets, function(dataset){ dataCollection = dataset.points || dataset.bars || dataset.segments; - if (dataCollection[dataIndex]){ + if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ Elements.push(dataCollection[dataIndex]); } }); helpers.each(Elements, function(element) { @@ -926,13 +1120,14 @@ titleFontSize: this.options.tooltipTitleFontSize, cornerRadius: this.options.tooltipCornerRadius, labels: tooltipLabels, legendColors: tooltipColors, legendColorBackground : this.options.multiTooltipKeyBackground, - title: ChartElements[0].label, + title: template(this.options.tooltipTitleTemplate,ChartElements[0]), chart: this.chart, - ctx: this.chart.ctx + ctx: this.chart.ctx, + custom: this.options.customTooltips }).draw(); } else { each(ChartElements, function(Element) { var tooltipPosition = Element.tooltipPosition(); @@ -947,11 +1142,12 @@ 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 + chart: this.chart, + custom: this.options.customTooltips }).draw(); }, this); } } return this; @@ -1040,19 +1236,22 @@ tooltipPosition : function(){ return { x : this.x, y : this.y }; + }, + hasValue: function(){ + return isNumber(this.value); } }); Chart.Element.extend = inherits; Chart.Point = Chart.Element.extend({ - display : true, - inRange : function(chartX,chartY){ + display: true, + 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(){ if (this.display){ @@ -1070,11 +1269,10 @@ 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(); @@ -1087,10 +1285,11 @@ // 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.x, this.y); // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); // ctx.stroke(); // ctx.restore(); @@ -1105,13 +1304,22 @@ var pointRelativePosition = helpers.getAngleFromPoint(this, { x: chartX, y: chartY }); + // Normalize all angles to 0 - 2*PI (0 - 360°) + var pointRelativeAngle = pointRelativePosition.angle % (Math.PI * 2), + startAngle = (Math.PI * 2 + this.startAngle) % (Math.PI * 2), + endAngle = (Math.PI * 2 + this.endAngle) % (Math.PI * 2) || 360; + + // Calculate wether the pointRelativeAngle is between the start and the end angle + var betweenAngles = (endAngle < startAngle) ? + pointRelativeAngle <= endAngle || pointRelativeAngle >= startAngle: + pointRelativeAngle >= startAngle && pointRelativeAngle <= endAngle; + //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); + var 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(){ @@ -1128,13 +1336,13 @@ 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.outerRadius < 0 ? 0 : this.outerRadius, this.startAngle, this.endAngle); - ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); + ctx.arc(this.x, this.y, this.innerRadius < 0 ? 0 : this.innerRadius, this.endAngle, this.startAngle, true); ctx.closePath(); ctx.strokeStyle = this.strokeColor; ctx.lineWidth = this.strokeWidth; @@ -1189,10 +1397,20 @@ inRange : function(chartX,chartY){ return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); } }); + Chart.Animation = Chart.Element.extend({ + currentStep: null, // the current animation step + numSteps: 60, // default number of steps + easing: "", // the easing to use for this animation + render: null, // render function used by the animation service + + onAnimationProgress: null, // user specified callback to fire on each step of the animation + onAnimationComplete: null, // user specified callback to fire when the animation finishes + }); + Chart.Tooltip = Chart.Element.extend({ draw : function(){ var ctx = this.chart.ctx; @@ -1200,11 +1418,11 @@ this.xAlign = "center"; this.yAlign = "above"; //Distance between the actual element.y position and the start of the tooltip caret - var caretPadding = 2; + var caretPadding = this.caretPadding = 2; var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, tooltipRectHeight = this.fontSize + 2*this.yPadding, tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; @@ -1222,61 +1440,68 @@ 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; + // Custom Tooltips + if(this.custom){ + this.custom(this); } + else{ + 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; - } + 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); + drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); - ctx.fill(); + ctx.fill(); - ctx.fillStyle = this.textColor; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); + 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.titleHeight = this.title ? this.titleFontSize * 1.5 : 0; + this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleHeight; this.ctx.font = this.titleFont; var titleWidth = this.ctx.measureText(this.title).width, //Label has a legend square as well so account for this. @@ -1287,11 +1512,10 @@ 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; } @@ -1309,47 +1533,53 @@ 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; + return baseLineHeight + this.titleHeight / 3; } else{ - return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; + return baseLineHeight + ((this.fontSize * 1.5 * afterTitleIndex) + this.fontSize / 2) + this.titleHeight; } }, 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(); + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + 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.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.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)); + 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. + //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.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); + ctx.fillStyle = this.legendColors[index].fill; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); - },this); + },this); + } } }); Chart.Scale = Chart.Element.extend({ initialize : function(){ @@ -1361,11 +1591,11 @@ 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; + this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) + 10 : 0; }, addXLabel : function(label){ this.xLabels.push(label); this.valuesCount++; this.fit(); @@ -1385,10 +1615,13 @@ // Apply padding settings to the start and end point. this.startPoint += this.padding; this.endPoint -= this.padding; + // Cache the starting endpoint, excluding the space for x labels + var cachedEndPoint = this.endPoint; + // Cache the starting height, so can determine if we need to recalculate the scale yAxis var cachedHeight = this.endPoint - this.startPoint, cachedYLabelWidth; // Build the current yLabels so we have an idea of what size they'll be to start @@ -1416,10 +1649,11 @@ this.calculateYRange(cachedHeight); this.buildYLabels(); // Only go through the xLabel loop again if the yLabel width has changed if (cachedYLabelWidth < this.yLabelWidth){ + this.endPoint = cachedEndPoint; this.calculateXLabelRotation(); } } }, @@ -1434,11 +1668,11 @@ firstRotated, lastRotated; this.xScalePaddingRight = lastWidth/2 + 3; - this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; + this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth) ? firstWidth/2 : this.yLabelWidth; this.xLabelRotation = 0; if (this.display){ var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), cosRotation, @@ -1453,11 +1687,11 @@ firstRotated = cosRotation * firstWidth; lastRotated = cosRotation * lastWidth; // We're right aligning the text now. - if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ + if (firstRotated + this.fontSize / 2 > this.yLabelWidth){ this.xScalePaddingLeft = firstRotated + this.fontSize / 2; } this.xScalePaddingRight = this.fontSize/2; @@ -1488,11 +1722,11 @@ }, 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)), + valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), valueOffset = (valueWidth * index) + this.xScalePaddingLeft; if (this.offsetGridLines){ valueOffset += (valueWidth/2); } @@ -1510,18 +1744,28 @@ 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); + linePositionY = Math.round(yLabelCenter), + drawHorizontalLine = this.showHorizontalLines; ctx.textAlign = "right"; ctx.textBaseline = "middle"; if (this.showLabels){ ctx.fillText(labelString,xStart - 10,yLabelCenter); } - ctx.beginPath(); + + // This is X axis, so draw it + if (index === 0 && !drawHorizontalLine){ + drawHorizontalLine = true; + } + + if (drawHorizontalLine){ + 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 { @@ -1530,14 +1774,16 @@ ctx.strokeStyle = this.lineColor; } linePositionY += helpers.aliasPixel(ctx.lineWidth); - ctx.moveTo(xStart, linePositionY); - ctx.lineTo(this.width, linePositionY); - ctx.stroke(); - ctx.closePath(); + if(drawHorizontalLine){ + 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); @@ -1549,29 +1795,40 @@ 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); + isRotated = (this.xLabelRotation > 0), + drawVerticalLine = this.showVerticalLines; - ctx.beginPath(); + // This is Y axis, so draw it + if (index === 0 && !drawVerticalLine){ + drawVerticalLine = true; + } + if (drawVerticalLine){ + 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(); + if (drawVerticalLine){ + 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 @@ -1812,18 +2069,44 @@ if (!this.lineArc){ ctx.lineWidth = this.angleLineWidth; ctx.strokeStyle = this.angleLineColor; for (var i = this.valuesCount - 1; i >= 0; i--) { + var centerOffset = null, outerPosition = null; + if (this.angleLineWidth > 0){ - var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); + centerOffset = this.calculateCenterOffset(this.max); + outerPosition = this.getPointPosition(i, centerOffset); ctx.beginPath(); ctx.moveTo(this.xCenter, this.yCenter); ctx.lineTo(outerPosition.x, outerPosition.y); ctx.stroke(); ctx.closePath(); } + + if (this.backgroundColors && this.backgroundColors.length == this.valuesCount) { + if (centerOffset == null) + centerOffset = this.calculateCenterOffset(this.max); + + if (outerPosition == null) + outerPosition = this.getPointPosition(i, centerOffset); + + var previousOuterPosition = this.getPointPosition(i === 0 ? this.valuesCount - 1 : i - 1, centerOffset); + var nextOuterPosition = this.getPointPosition(i === this.valuesCount - 1 ? 0 : i + 1, centerOffset); + + var previousOuterHalfway = { x: (previousOuterPosition.x + outerPosition.x) / 2, y: (previousOuterPosition.y + outerPosition.y) / 2 }; + var nextOuterHalfway = { x: (outerPosition.x + nextOuterPosition.x) / 2, y: (outerPosition.y + nextOuterPosition.y) / 2 }; + + ctx.beginPath(); + ctx.moveTo(this.xCenter, this.yCenter); + ctx.lineTo(previousOuterHalfway.x, previousOuterHalfway.y); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.lineTo(nextOuterHalfway.x, nextOuterHalfway.y); + ctx.fillStyle = this.backgroundColors[i]; + ctx.fill(); + 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; @@ -1856,10 +2139,97 @@ } } } }); + Chart.animationService = { + frameDuration: 17, + animations: [], + dropFrames: 0, + addAnimation: function(chartInstance, animationObject) { + for (var index = 0; index < this.animations.length; ++ index){ + if (this.animations[index].chartInstance === chartInstance){ + // replacing an in progress animation + this.animations[index].animationObject = animationObject; + return; + } + } + + this.animations.push({ + chartInstance: chartInstance, + animationObject: animationObject + }); + + // If there are no animations queued, manually kickstart a digest, for lack of a better word + if (this.animations.length == 1) { + helpers.requestAnimFrame.call(window, this.digestWrapper); + } + }, + // Cancel the animation for a given chart instance + cancelAnimation: function(chartInstance) { + var index = helpers.findNextWhere(this.animations, function(animationWrapper) { + return animationWrapper.chartInstance === chartInstance; + }); + + if (index) + { + this.animations.splice(index, 1); + } + }, + // calls startDigest with the proper context + digestWrapper: function() { + Chart.animationService.startDigest.call(Chart.animationService); + }, + startDigest: function() { + + var startTime = Date.now(); + var framesToDrop = 0; + + if(this.dropFrames > 1){ + framesToDrop = Math.floor(this.dropFrames); + this.dropFrames -= framesToDrop; + } + + for (var i = 0; i < this.animations.length; i++) { + + if (this.animations[i].animationObject.currentStep === null){ + this.animations[i].animationObject.currentStep = 0; + } + + this.animations[i].animationObject.currentStep += 1 + framesToDrop; + if(this.animations[i].animationObject.currentStep > this.animations[i].animationObject.numSteps){ + this.animations[i].animationObject.currentStep = this.animations[i].animationObject.numSteps; + } + + this.animations[i].animationObject.render(this.animations[i].chartInstance, this.animations[i].animationObject); + + // Check if executed the last frame. + if (this.animations[i].animationObject.currentStep == this.animations[i].animationObject.numSteps){ + // Call onAnimationComplete + this.animations[i].animationObject.onAnimationComplete.call(this.animations[i].chartInstance); + // Remove the animation. + this.animations.splice(i, 1); + // Keep the index in place to offset the splice + i--; + } + } + + var endTime = Date.now(); + var delay = endTime - startTime - this.frameDuration; + var frameDelay = delay / this.frameDuration; + + if(frameDelay > 1){ + this.dropFrames += frameDelay; + } + + // Do we have more stuff to animate? + if (this.animations.length > 0){ + helpers.requestAnimFrame.call(window, this.digestWrapper); + } + } + }; + // Attach global event to resize each chart instance when the browser resizes helpers.addEvent(window, "resize", (function(){ // Basic debounce of resize function so it doesn't hurt performance when resizing browser. var timeout; return function(){ @@ -1913,10 +2283,16 @@ scaleGridLineColor : "rgba(237,239,241,1)", //Number - Width of the grid lines scaleGridLineWidth : 1, + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + //Boolean - If there is a stroke on each bar barShowStroke : true, //Number - Pixel width of the bar stroke barStrokeWidth : 2, @@ -1926,11 +2302,11 @@ //Number - Spacing between data sets within X values barDatasetSpacing : 1, //String - A legend template - legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].fillColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>" + legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].fillColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>" }; Chart.Type.extend({ @@ -1998,22 +2374,20 @@ }; 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], - datasetLabel: dataset.label, - strokeColor : dataset.strokeColor, - fillColor : dataset.fillColor, - highlightFill : dataset.highlightFill || dataset.fillColor, - highlightStroke : dataset.highlightStroke || dataset.strokeColor - })); - } + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.bars.push(new this.BarClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.strokeColor, + fillColor : dataset.fillColor, + highlightFill : dataset.highlightFill || dataset.fillColor, + highlightStroke : dataset.highlightStroke || dataset.strokeColor + })); },this); },this); this.buildScale(data.labels); @@ -2102,10 +2476,12 @@ }, xLabels : labels, font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), lineWidth : this.options.scaleLineWidth, lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, showLabels : this.options.scaleShowLabels, display : this.options.showScale @@ -2124,23 +2500,22 @@ 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 - })); - } + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].bars.push(new this.BarClass({ + value : value, + label : label, + datasetLabel: this.datasets[datasetIndex].label, + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), + y: this.scale.endPoint, + width : this.scale.calculateBarWidth(this.datasets.length), + base : this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].strokeColor, + fillColor : this.datasets[datasetIndex].fillColor + })); },this); this.scale.addXLabel(label); //Then re-render the chart. this.update(); @@ -2173,25 +2548,28 @@ 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(); + if (bar.hasValue()){ + bar.base = this.scale.endPoint; + //Transition then draw + bar.transition({ + x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y : this.scale.calculateY(bar.value), + width : this.scale.calculateBarWidth(this.datasets.length) + }, easingDecimal).draw(); + } },this); },this); } }); }).call(this); + (function(){ "use strict"; var root = this, Chart = root.Chart, @@ -2222,15 +2600,14 @@ //Boolean - Whether we animate scaling the Doughnut from the centre animateScale : false, //String - A legend template - legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>" + legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"><%if(segments[i].label){%><%=segments[i].label%><%}%></span></li><%}%></ul>" }; - 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, @@ -2263,10 +2640,13 @@ }); } this.calculateTotal(data); helpers.each(data,function(datapoint, index){ + if (!datapoint.color) { + datapoint.color = 'hsl(' + (360 * index / data.length) + ', 100%, 50%)'; + } this.addData(datapoint, index, true); },this); this.render(); }, @@ -2279,11 +2659,15 @@ if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); },this); return segmentsArray; }, addData : function(segment, atIndex, silent){ - var index = atIndex || this.segments.length; + var index = atIndex !== undefined ? atIndex : this.segments.length; + if ( typeof(segment.color) === "undefined" ) { + segment.color = Chart.defaults.global.segmentColorDefault[index % Chart.defaults.global.segmentColorDefault.length]; + segment.highlight = Chart.defaults.global.segmentHighlightColorDefaults[index % Chart.defaults.global.segmentHighlightColorDefaults.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, @@ -2298,17 +2682,21 @@ if (!silent){ this.reflow(); this.update(); } }, - calculateCircumference : function(value){ - return (Math.PI*2)*(value / this.total); + calculateCircumference : function(value) { + if ( this.total > 0 ) { + return (Math.PI*2)*(value / this.total); + } else { + return 0; + } }, calculateTotal : function(data){ this.total = 0; helpers.each(data,function(segment){ - this.total += segment.value; + this.total += Math.abs(segment.value); },this); }, update : function(){ this.calculateTotal(this.segments); @@ -2372,10 +2760,11 @@ name : "Pie", defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) }); }).call(this); + (function(){ "use strict"; var root = this, Chart = root.Chart, @@ -2390,10 +2779,16 @@ scaleGridLineColor : "rgba(237,239,241,1)", //Number - Width of the grid lines scaleGridLineWidth : 1, + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + //Boolean - Whether the line is curved between points bezierCurve : false, //Number - Tension of the bezier curve between points bezierCurveTension : 0.4, @@ -2418,24 +2813,28 @@ //Boolean - Whether to fill the dataset with a colour datasetFill : true, //String - A legend template - legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>" + legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>", + //Boolean - Whether to horizontally center the label and point dot inside the grid + offsetGridLines : false + }; 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({ + offsetGridLines : this.options.offsetGridLines, strokeWidth : this.options.pointDotStrokeWidth, radius : this.options.pointDotRadius, - display : this.options.pointDot, + display: this.options.pointDot, 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)); } @@ -2472,25 +2871,20 @@ 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, - datasetLabel: dataset.label, - strokeColor : dataset.pointStrokeColor, - fillColor : dataset.pointColor, - highlightFill : dataset.pointHighlightFill || dataset.pointColor, - highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor - })); - } + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); },this); this.buildScale(data.labels); @@ -2549,10 +2943,11 @@ templateString : this.options.scaleLabel, height : this.chart.height, width : this.chart.width, ctx : this.chart.ctx, textColor : this.options.scaleFontColor, + offsetGridLines : this.options.offsetGridLines, fontSize : this.options.scaleFontSize, fontStyle : this.options.scaleFontStyle, fontFamily : this.options.scaleFontFamily, valuesCount : labels.length, beginAtZero : this.options.scaleBeginAtZero, @@ -2569,10 +2964,12 @@ }, xLabels : labels, font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), lineWidth : this.options.scaleLineWidth, lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, showLabels : this.options.scaleShowLabels, display : this.options.showScale @@ -2593,21 +2990,20 @@ }, 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 - })); - } + //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, + datasetLabel: this.datasets[datasetIndex].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(); @@ -2631,96 +3027,128 @@ var easingDecimal = ease || 1; this.clear(); var ctx = this.chart.ctx; + // Some helper methods for getting the next/prev points + var hasValue = function(item){ + return item.value !== null; + }, + nextPoint = function(point, collection, index){ + return helpers.findNextWhere(collection, hasValue, index) || point; + }, + previousPoint = function(point, collection, index){ + return helpers.findPreviousWhere(collection, hasValue, index) || point; + }; + + if (!this.scale) return; this.scale.draw(easingDecimal); helpers.each(this.datasets,function(dataset){ + var pointsWithValues = helpers.where(dataset.points, hasValue); //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); - + helpers.each(dataset.points, function(point, index){ + if (point.hasValue()){ + 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 + // Control points need to be calculated in a separate loop, because we need to know the current x/y of the point // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed if (this.options.bezierCurve){ - helpers.each(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); + helpers.each(pointsWithValues, function(point, index){ + var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; + point.controlPoints = helpers.splineCurve( + previousPoint(point, pointsWithValues, index), + point, + nextPoint(point, pointsWithValues, index), + tension + ); + + // Prevent the bezier going outside of the bounds of the graph + + // Cap puter bezier handles to the upper/lower scale bounds + if (point.controlPoints.outer.y > this.scale.endPoint){ + point.controlPoints.outer.y = this.scale.endPoint; } - else if (index >= dataset.points.length-1){ - point.controlPoints = helpers.splineCurve(dataset.points[index-1],point,point,0); + else if (point.controlPoints.outer.y < this.scale.startPoint){ + point.controlPoints.outer.y = this.scale.startPoint; } - else{ - point.controlPoints = helpers.splineCurve(dataset.points[index-1],point,dataset.points[index+1],this.options.bezierCurveTension); + + // Cap inner bezier handles to the upper/lower scale bounds + if (point.controlPoints.inner.y > this.scale.endPoint){ + point.controlPoints.inner.y = this.scale.endPoint; } + else if (point.controlPoints.inner.y < this.scale.startPoint){ + point.controlPoints.inner.y = this.scale.startPoint; + } },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){ + + helpers.each(pointsWithValues, function(point, index){ + if (index === 0){ + ctx.moveTo(point.x, point.y); + } + else{ if(this.options.bezierCurve){ + var previous = previousPoint(point, pointsWithValues, index); + ctx.bezierCurveTo( - dataset.points[index-1].controlPoints.outer.x, - dataset.points[index-1].controlPoints.outer.y, + previous.controlPoints.outer.x, + previous.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(); + }, this); + if (this.options.datasetStroke) { + ctx.stroke(); + } - if (this.options.datasetFill){ + if (this.options.datasetFill && pointsWithValues.length > 0){ //Round off the line by going to the base of the chart, back to the start, then fill. - ctx.lineTo(dataset.points[dataset.points.length-1].x, this.scale.endPoint); - ctx.lineTo(this.scale.calculateX(0), this.scale.endPoint); + ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); + ctx.lineTo(pointsWithValues[0].x, 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){ + helpers.each(pointsWithValues,function(point){ point.draw(); }); - },this); } }); }).call(this); + (function(){ "use strict"; var root = this, Chart = root.Chart, @@ -2747,11 +3175,11 @@ scaleShowLine : true, //Boolean - Stroke a line around each segment in the chart segmentShowStroke : true, - //String - The colour of the stroke on each segement. + //String - The colour of the stroke on each segment. segmentStrokeColor : "rgba(255,255,255,1)", //Number - The width of the stroke value in pixels segmentStrokeWidth : 2, @@ -2766,11 +3194,11 @@ //Boolean - Whether to animate scaling the chart from the centre animateScale : false, //String - A legend template - legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>" + legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"><%if(segments[i].label){%><%=segments[i].label%><%}%></span></li><%}%></ul>" }; Chart.Type.extend({ //Passing in a name registers this chart in the Chart namespace @@ -2914,10 +3342,12 @@ this.calculateTotal(this.segments); helpers.each(this.segments,function(segment){ segment.save(); }); + + this.reflow(); this.render(); }, reflow : function(){ helpers.extend(this.SegmentArc.prototype,{ x : this.chart.width/2, @@ -2965,10 +3395,11 @@ this.scale.draw(); } }); }).call(this); + (function(){ "use strict"; var root = this, Chart = root.Chart, @@ -2996,11 +3427,11 @@ //Number - Pixel width of the angle line angleLineWidth : 1, //String - Point label font declaration - pointLabelFontFamily : "'Gotham', 'Gotham Round', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + pointLabelFontFamily : "'Gotham Round', 'Gotham', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", //String - Point label font weight pointLabelFontStyle : "bold", //Number - Point label font size in pixels @@ -3029,19 +3460,19 @@ //Boolean - Whether to fill the dataset with a colour datasetFill : true, //String - A legend template - legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>" + legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>" }, initialize: function(data){ this.PointClass = Chart.Point.extend({ strokeWidth : this.options.pointDotStrokeWidth, radius : this.options.pointDotRadius, - display : this.options.pointDot, + display: this.options.pointDot, hitDetectionRadius : this.options.pointHitDetectionRadius, ctx : this.chart.ctx }); this.datasets = []; @@ -3069,38 +3500,35 @@ helpers.each(data.datasets,function(dataset){ var datasetObject = { label: dataset.label || null, fillColor : dataset.fillColor, - datasetLabel: dataset.label, 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 - })); + 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], + datasetLabel: dataset.label, + 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(); @@ -3144,10 +3572,11 @@ fontFamily: this.options.scaleFontFamily, fontColor: this.options.scaleFontColor, showLabels: this.options.scaleShowLabels, showLabelBackdrop: this.options.scaleShowLabelBackdrop, backdropColor: this.options.scaleBackdropColor, + backgroundColors: this.options.scaleBackgroundColors, backdropPaddingY : this.options.scaleBackdropPaddingY, backdropPaddingX: this.options.scaleBackdropPaddingX, lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, lineColor: this.options.scaleLineColor, angleLineColor : this.options.angleLineColor, @@ -3211,21 +3640,20 @@ }, 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 - })); - } + var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + datasetLabel: this.datasets[datasetIndex].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(); @@ -3268,11 +3696,13 @@ 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); + if (point.hasValue()){ + point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); + } },this); //Draw the line between all the points @@ -3289,16 +3719,19 @@ },this); ctx.closePath(); ctx.stroke(); ctx.fillStyle = dataset.fillColor; - ctx.fill(); - + if(this.options.datasetFill){ + 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(); + if (point.hasValue()){ + point.draw(); + } }); },this); } \ No newline at end of file