diff --git a/Chart.js b/Chart.js index 88b54859a..4ab301d15 100644 --- a/Chart.js +++ b/Chart.js @@ -11,2900 +11,4334 @@ (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 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; - - // Firefox requires this to work correctly - context.canvas.width = width; - context.canvas.height = 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; - }; - - var defaultColor = 'rgba(0,0,0,0.1)'; - - //Globally expose the defaults to allow for user updating/changing - Chart.defaults = { - global: { - responsive: true, - maintainAspectRatio: true, - events: ["mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"], - hover: { - onHover: null, - mode: 'single', - animationDuration: 400, - }, - onClick: null, - defaultColor: defaultColor, - - // Element defaults defined in element extensions - elements: {} - }, - }; - - //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 < loopable.length; i++) { - callback.apply(self, [loopable[i], i].concat(additionalArgs)); - } - } else { - for (var item in loopable) { - callback.apply(self, [loopable[item], item].concat(additionalArgs)); - } - } - } - }, - 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); - }, - // Need a special merge function to chart configs since they are now grouped - configMerge = helpers.configMerge = function(_base) { - var base = clone(_base); - helpers.each(Array.prototype.slice.call(arguments, 1), function(extension) { - helpers.each(extension, function(value, key) { - if (extension.hasOwnProperty(key)) { - if (base.hasOwnProperty(key) && helpers.isArray(base[key]) && helpers.isArray(value)) { - // In this case we have an array of objects replacing another array. Rather than doing a strict replace, - // merge. This allows easy scale option merging - var baseArray = base[key]; - - helpers.each(value, function(valueObj, index) { - if (index < baseArray.length) { - baseArray[index] = helpers.configMerge(baseArray[index], valueObj); - } else { - baseArray.push(valueObj); // nothing to merge - } - }); - } else if (base.hasOwnProperty(key) && typeof base[key] == "object" && base[key] !== null && typeof value == "object") { - // If we are overwriting an object with an object, do a merge of the properties. - base[key] = helpers.configMerge(base[key], value); - } else { - // can just overwrite the value in this case - base[key] = value; - } - } - }); - }); - - return base; - }, - getValueAtIndexOrDefault = helpers.getValueAtIndexOrDefault = function(value, index, defaultValue) { - if (!value) { - return defaultValue; - } - - if (helpers.isArray(value) && index < value.length) { - return value[index]; - } - - return value; - }, - indexOf = helpers.indexOf = function(arrayToSearch, item) { - if (Array.prototype.indexOf) { - return arrayToSearch.indexOf(item); - } else { - for (var i = 0; i < arrayToSearch.length; i++) { - 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); - }; - - var Surrogate = function() { - this.constructor = ChartElement; - }; - Surrogate.prototype = parent.prototype; - ChartElement.prototype = new Surrogate(); - - ChartElement.extend = inherits; - - if (extensions) extend(ChartElement.prototype, extensions); - - ChartElement.__super__ = parent.prototype; - - return ChartElement; - }, - noop = helpers.noop = function() {}, - uid = helpers.uid = (function() { - var id = 0; - return function() { - return "chart-" + id++; - }; - })(), - warn = helpers.warn = function(str) { - //Method for warning of errors - if (window.console && typeof window.console.warn === "function") console.warn(str); - }, - 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) { - return Math.max.apply(Math, array); - }, - min = helpers.min = function(array) { - return Math.min.apply(Math, array); - }, - sign = helpers.sign = function(x) { - if (Math.sign) { - return Math.sign(x); - } else { - x = +x; // convert to a number - if (x === 0 || isNaN(x)) { - return x; - } - return x > 0 ? 1 : -1; - } - }, - log10 = helpers.log10 = function(x) { - if (Math.log10) { - return Math.log10(x) - } else { - return Math.log(x) / Math.LN10; - } - }, - cap = helpers.cap = function(valueToCap, maxValue, minValue) { - if (isNumber(maxValue)) { - if (valueToCap > 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)) { - 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; - } - }, - toRadians = helpers.toRadians = function(degrees) { - return degrees * (Math.PI / 180); - }, - toDegrees = helpers.toDegrees = function(radians) { - return radians * (180 / Math.PI); - }, - // 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.atan2(distanceFromYCenter, distanceFromXCenter); - - if (angle < (-0.5 * Math.PI)) { - angle += 2.0 * Math.PI; // make sure the returned angle is in the range of (-PI/2, 3PI/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 { - previous: { - x: MiddlePoint.x - fa * (AfterPoint.x - FirstPoint.x), - y: MiddlePoint.y - fa * (AfterPoint.y - FirstPoint.y) - }, - next: { - 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) - }; - - }, - // Implementation of the nice number algorithm used in determining where axis labels will go - niceNum = helpers.niceNum = function(range, round) { - var exponent = Math.floor(helpers.log10(range)); - var fraction = range / Math.pow(10, exponent); - var niceFraction; - - if (round) { - if (fraction < 1.5) { - niceFraction = 1; - } else if (fraction < 3) { - niceFraction = 2; - } else if (fraction < 7) { - niceFraction = 5; - } else { - niceFraction = 10; - } - } else { - if (fraction <= 1.0) { - niceFraction = 1; - } else if (fraction <= 2) { - niceFraction = 2; - } else if (fraction <= 5) { - niceFraction = 5; - } else { - niceFraction = 10; - } - } - - return niceFraction * Math.pow(10, exponent); - }, - /* 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) { - return 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 (templateString) { - 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); - }); - }, - getMaximumWidth = helpers.getMaximumWidth = function(domNode) { - 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 - padding; - }, - getMaximumHeight = helpers.getMaximumHeight = function(domNode) { - 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 - 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; - - 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(); - }, - color = helpers.color = function(color) { - if (!window.Color) { - console.log('Color.js not found!'); - return color; - } - return window.Color(color); - }, - isArray = helpers.isArray = function(obj) { - if (!Array.isArray) { - return Object.prototype.toString.call(arg) === '[object Array]'; - } - return Array.isArray(obj); - }; - - //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(config, instance) { - this.data = config.data; - this.options = config.options; - this.chart = instance; - 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 (this.options.responsive) { - this.resize(); - } - this.initialize.call(this); - }; - - //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 - Chart.animationService.cancelAnimation(this); - return this; - }, - resize: function() { - 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; - - retinaScale(this.chart); - - return this; - }, - redraw: noop, - render: function(duration) { - - if (this.options.animation.duration !== 0 || duration) { - var animation = new Chart.Animation(); - animation.numSteps = (duration || this.options.animation.duration) / 16.66; //60 fps - animation.easing = this.options.animation.easing; - - // 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, duration); - } else { - this.draw(); - this.options.onAnimationComplete.call(this); - } - return this; - }, - eachElement: function(callback) { - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - helpers.each(dataset.metaData, callback, this, dataset.metaData, datasetIndex); - }, this); - }, - eachValue: function(callback) { - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - helpers.each(dataset.data, callback, this, datasetIndex); - }, this); - }, - eachDataset: function(callback) { - helpers.each(this.data.datasets, callback, this); - }, - getElementsAtEvent: function(e) { - var elementsArray = [], - eventPosition = helpers.getRelativePosition(e), - datasetIterator = function(dataset) { - elementsArray.push(dataset.metaData[elementIndex]); - }, - elementIndex; - - for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; datasetIndex++) { - for (elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; elementIndex++) { - if (this.data.datasets[datasetIndex].metaData[elementIndex].inGroupRange(eventPosition.x, eventPosition.y)) { - helpers.each(this.data.datasets, datasetIterator); - } - } - } - - return elementsArray.length ? elementsArray : []; - }, - // Get the single element that was clicked on - // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was drawn - getElementAtEvent: function(e) { - var element = []; - var eventPosition = helpers.getRelativePosition(e); - - for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; ++datasetIndex) { - for (var elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; ++elementIndex) { - if (this.data.datasets[datasetIndex].metaData[elementIndex].inRange(eventPosition.x, eventPosition.y)) { - element.push(this.data.datasets[datasetIndex].metaData[elementIndex]); - return element; - } - } - } - - return []; - }, - generateLegend: function() { - 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]; - }, - 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] = helpers.configMerge(baseDefaults, extensions.defaults); - - Chart.types[chartName] = ChartType; - - //Register this new chart type in the Chart prototype - Chart.prototype[chartName] = function(config) { - config.options = helpers.configMerge(Chart.defaults.global, Chart.defaults[chartName], config.options || {}); - return new ChartType(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); - }; - extend(Chart.Element.prototype, { - initialize: function() {}, - pivot: function() { - if (!this._view) { - this._view = clone(this._model); - } - this._start = clone(this._view); - return this; - }, - transition: function(ease) { - if (!this._view) { - this._view = clone(this._model); - } - if (!this._start) { - this.pivot(); - } - - each(this._model, function(value, key) { - - if (key[0] === '_' || !this._model.hasOwnProperty(key)) { - // Only non-underscored properties - } - - // Init if doesn't exist - else if (!this._view[key]) { - if (typeof value === 'number') { - this._view[key] = value * ease; - } else { - this._view[key] = value || null; - } - } - - // No unnecessary computations - else if (this._model[key] === this._view[key]) { - // It's the same! Woohoo! - } - - // Color transitions if possible - else if (typeof value === 'string') { - try { - var color = helpers.color(this._start[key]).mix(helpers.color(this._model[key]), ease); - this._view[key] = color.rgbString(); - } catch (err) { - this._view[key] = value; - } - } - // Number transitions - else if (typeof value === 'number') { - var startVal = this._start[key] !== undefined ? this._start[key] : 0; - this._view[key] = ((this._model[key] - startVal) * ease) + startVal; - } - // Everything else - else { - this._view[key] = value; - } - - }, this); - - if (ease === 1) { - delete this._start; - } - return this; - }, - tooltipPosition: function() { - return { - x: this._model.x, - y: this._model.y - }; - }, - hasValue: function() { - return isNumber(this._model.x) && isNumber(this._model.y); - } - }); - - Chart.Element.extend = inherits; - - - // 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.update(); - instance.render(); - } - }); - }, 50); - }; - })()); - - - if (amd) { - define(function() { - return Chart; - }); - } else if (typeof module === 'object' && module.exports) { - module.exports = Chart; - } - - root.Chart = Chart; - - Chart.noConflict = function() { - root.Chart = previous; - return Chart; - }; - -}).call(this); - -/*! - * Chart.js - * http://chartjs.org/ - * Version: 2.0.0-alpha - * - * Copyright 2015 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ - - -(function() { - - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - Chart.defaults.global.animation = { - duration: 1000, - easing: "easeOutQuart", - onProgress: function() {}, - onComplete: function() {}, - }; - - 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.animationService = { - frameDuration: 17, - animations: [], - dropFrames: 0, - addAnimation: function(chartInstance, animationObject, duration) { - - if (!duration) { - chartInstance.animating = true; - } - - 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); - chartInstance.animating = false; - } - }, - // calls startDigest with the proper context - digestWrapper: function() { - Chart.animationService.startDigest.call(Chart.animationService); - }, - startDigest: function() { - - var startTime = Date.now(); - var framesToDrop = 0; - - if (this.dropFrames > 1) { - framesToDrop = Math.floor(this.dropFrames); - this.dropFrames -= framesToDrop; - } - - for (var i = 0; i < this.animations.length; i++) { - - if (this.animations[i].animationObject.currentStep === null) { - this.animations[i].animationObject.currentStep = 0; - } - - this.animations[i].animationObject.currentStep += 1 + framesToDrop; - if (this.animations[i].animationObject.currentStep > this.animations[i].animationObject.numSteps) { - this.animations[i].animationObject.currentStep = this.animations[i].animationObject.numSteps; - } - - this.animations[i].animationObject.render(this.animations[i].chartInstance, this.animations[i].animationObject); - - if (this.animations[i].animationObject.currentStep == this.animations[i].animationObject.numSteps) { - // executed the last frame. Remove the animation. - this.animations[i].chartInstance.animating = false; - 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); - } - } - }; + "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, config) { + var chart = this; + this.config = config; + + // Support a jQuery'd canvas element + if (context.length && context[0].getContext) { + context = context[0]; + } + + // Support a canvas domnode + if (context.getContext) { + context = context.getContext("2d"); + } + + this.canvas = context.canvas; + + this.ctx = context; + + //Variables global to the chart + 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; + + // Firefox requires this to work correctly + context.canvas.width = width; + context.canvas.height = 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. + Chart.helpers.retinaScale(this); + + if (config) { + this.controller = new Chart.Controller(this); + return this.controller; + } + + return this; + + }; + + //Globally expose the defaults to allow for user updating/changing + Chart.defaults = { + global: { + responsive: true, + maintainAspectRatio: true, + events: ["mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"], + hover: { + onHover: null, + mode: 'single', + animationDuration: 400, + }, + onClick: null, + defaultColor: 'rgba(0,0,0,0.1)', + + // Element defaults defined in element extensions + elements: {} + }, + }; + + if (typeof amd !== 'undefined') { + define(function() { + return Chart; + }); + } else if (typeof module === 'object' && module.exports) { + module.exports = 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; - - // The scale service is used to resize charts along with all of their axes. We make this as - // a service where scales are registered with their respective charts so that changing the - // scales does not require - Chart.scaleService = { - // The interesting function - fitScalesForChart: function(chartInstance, width, height) { - var xPadding = width > 30 ? 5 : 2; - var yPadding = height > 30 ? 5 : 2; - - if (chartInstance) { - var leftScales = helpers.where(chartInstance.scales, function(scaleInstance) { - return scaleInstance.options.position == "left"; - }); - var rightScales = helpers.where(chartInstance.scales, function(scaleInstance) { - return scaleInstance.options.position == "right"; - }); - var topScales = helpers.where(chartInstance.scales, function(scaleInstance) { - return scaleInstance.options.position == "top"; - }); - var bottomScales = helpers.where(chartInstance.scales, function(scaleInstance) { - return scaleInstance.options.position == "bottom"; - }); - - var visibleLeftScales = helpers.where(chartInstance.scales, function(scaleInstance) { - return scaleInstance.options.position == "left"; - }); - var visibleRightScales = helpers.where(chartInstance.scales, function(scaleInstance) { - return scaleInstance.options.position == "right"; - }); - var visibleTopScales = helpers.where(chartInstance.scales, function(scaleInstance) { - return scaleInstance.options.position == "top"; - }); - var visibleBottomScales = helpers.where(chartInstance.scales, function(scaleInstance) { - return scaleInstance.options.position == "bottom"; - }); - - // // Adjust the padding to take into account displaying labels - // if (topScales.length === 0 || bottomScales.length === 0) { - // var maxFontHeight = 0; - - // var maxFontHeightFunction = function(scaleInstance) { - // if (scaleInstance.options.labels.show) { - // // Only consider font sizes for axes that actually show labels - // maxFontHeight = Math.max(maxFontHeight, scaleInstance.options.labels.fontSize); - // } - // }; - - // helpers.each(leftScales, maxFontHeightFunction); - // helpers.each(rightScales, maxFontHeightFunction); - - // if (topScales.length === 0) { - // // Add padding so that we can handle drawing the top nicely - // yPadding += 0.75 * maxFontHeight; // 0.75 since padding added on both sides - // } - - // if (bottomScales.length === 0) { - // // Add padding so that we can handle drawing the bottom nicely - // yPadding += 1.5 * maxFontHeight; - // } - // } - - // Essentially we now have any number of scales on each of the 4 sides. - // Our canvas looks like the following. - // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and - // B1 is the bottom axis - // |------------------------------------------------------| - // | | T1 | | - // |----|-----|-------------------------------------|-----| - // | | | | | - // | L1 | L2 | Chart area | R1 | - // | | | | | - // | | | | | - // |----|-----|-------------------------------------|-----| - // | | B1 | | - // | | | | - // |------------------------------------------------------| - - // What we do to find the best sizing, we do the following - // 1. Determine the minimum size of the chart area. - // 2. Split the remaining width equally between each vertical axis - // 3. Split the remaining height equally between each horizontal axis - // 4. Give each scale the maximum size it can be. The scale will return it's minimum size - // 5. Adjust the sizes of each axis based on it's minimum reported size. - // 6. Refit each axis - // 7. Position each axis in the final location - // 8. Tell the chart the final location of the chart area - - // Step 1 - var chartWidth = width / 2; // min 50% - var chartHeight = height / 2; // min 50% - - chartWidth -= (2 * xPadding); - chartHeight -= (2 * yPadding); - - - // Step 2 - var verticalScaleWidth = (width - chartWidth) / (leftScales.length + rightScales.length); - - // Step 3 - var horizontalScaleHeight = (height - chartHeight) / (topScales.length + bottomScales.length); - - // Step 4; - var minimumScaleSizes = []; - - var verticalScaleMinSizeFunction = function(scaleInstance) { - var minSize = scaleInstance.fit(verticalScaleWidth, chartHeight); - minimumScaleSizes.push({ - horizontal: false, - minSize: minSize, - scale: scaleInstance, - }); - }; - - var horizontalScaleMinSizeFunction = function(scaleInstance) { - var minSize = scaleInstance.fit(chartWidth, horizontalScaleHeight); - minimumScaleSizes.push({ - horizontal: true, - minSize: minSize, - scale: scaleInstance, - }); - }; - - // vertical scales - helpers.each(leftScales, verticalScaleMinSizeFunction); - helpers.each(rightScales, verticalScaleMinSizeFunction); - - // horizontal scales - helpers.each(topScales, horizontalScaleMinSizeFunction); - helpers.each(bottomScales, horizontalScaleMinSizeFunction); - - // Step 5 - var maxChartHeight = height - (2 * yPadding); - var maxChartWidth = width - (2 * xPadding); - - helpers.each(minimumScaleSizes, function(wrapper) { - if (wrapper.horizontal) { - maxChartHeight -= wrapper.minSize.height; - } else { - maxChartWidth -= wrapper.minSize.width; - } - }); - - // At this point, maxChartHeight and maxChartWidth are the size the chart area could - // be if the axes are drawn at their minimum sizes. - - // Step 6 - var verticalScaleFitFunction = function(scaleInstance) { - var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) { - return wrapper.scale === scaleInstance; - }); - - if (wrapper) { - scaleInstance.fit(wrapper.minSize.width, maxChartHeight); - } - }; - - var horizontalScaleFitFunction = function(scaleInstance) { - var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) { - return wrapper.scale === scaleInstance; - }); - - var scaleMargin = { - left: totalLeftWidth, - right: totalRightWidth, - top: 0, - bottom: 0, - }; - - if (wrapper) { - scaleInstance.fit(maxChartWidth, wrapper.minSize.height, scaleMargin); - } - }; - - var totalLeftWidth = xPadding; - var totalRightWidth = xPadding; - var totalTopHeight = yPadding; - var totalBottomHeight = yPadding; - - helpers.each(leftScales, verticalScaleFitFunction); - helpers.each(rightScales, verticalScaleFitFunction); - - // Figure out how much margin is on the left and right of the horizontal axes - helpers.each(leftScales, function(scaleInstance) { - totalLeftWidth += scaleInstance.width; - }); - - helpers.each(rightScales, function(scaleInstance) { - totalRightWidth += scaleInstance.width; - }); - - helpers.each(topScales, horizontalScaleFitFunction); - helpers.each(bottomScales, horizontalScaleFitFunction); - - helpers.each(topScales, function(scaleInstance) { - totalTopHeight += scaleInstance.height; - }); - helpers.each(bottomScales, function(scaleInstance) { - totalBottomHeight += scaleInstance.height; - }); - - // Let the left scale know the final margin - helpers.each(leftScales, function(scaleInstance) { - var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) { - return wrapper.scale === scaleInstance; - }); - - var scaleMargin = { - left: 0, - right: 0, - top: totalTopHeight, - bottom: totalBottomHeight - }; - - if (wrapper) { - scaleInstance.fit(wrapper.minSize.width, maxChartHeight, scaleMargin); - } - }); - - helpers.each(rightScales, function(scaleInstance) { - var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) { - return wrapper.scale === scaleInstance; - }); - - var scaleMargin = { - left: 0, - right: 0, - top: totalTopHeight, - bottom: totalBottomHeight - }; - - if (wrapper) { - scaleInstance.fit(wrapper.minSize.width, maxChartHeight, scaleMargin); - } - }); - - // Step 7 - // Position the scales - var left = xPadding; - var top = yPadding; - var right = 0; - var bottom = 0; - - var verticalScalePlacer = function(scaleInstance) { - scaleInstance.left = left; - scaleInstance.right = left + scaleInstance.width; - scaleInstance.top = totalTopHeight; - scaleInstance.bottom = totalTopHeight + maxChartHeight; - - // Move to next point - left = scaleInstance.right; - }; - - var horizontalScalePlacer = function(scaleInstance) { - scaleInstance.left = totalLeftWidth; - scaleInstance.right = totalLeftWidth + maxChartWidth; - scaleInstance.top = top; - scaleInstance.bottom = top + scaleInstance.height; - - // Move to next point - top = scaleInstance.bottom; - }; - - helpers.each(leftScales, verticalScalePlacer); - helpers.each(topScales, horizontalScalePlacer); - - // Account for chart width and height - left += maxChartWidth; - top += maxChartHeight; - - helpers.each(rightScales, verticalScalePlacer); - helpers.each(bottomScales, horizontalScalePlacer); - - // Step 8 - chartInstance.chartArea = { - left: totalLeftWidth, - top: totalTopHeight, - right: totalLeftWidth + maxChartWidth, - bottom: totalTopHeight + maxChartHeight, - }; - } - } - }; - - // Scale registration object. Extensions can register new scale types (such as log or DB scales) and then - // use the new chart options to grab the correct scale - Chart.scales = { - constructors: {}, - // Use a registration function so that we can move to an ES6 map when we no longer need to support - // old browsers - registerScaleType: function(type, scaleConstructor) { - this.constructors[type] = scaleConstructor; - }, - getScaleConstructor: function(type) { - return this.constructors.hasOwnProperty(type) ? this.constructors[type] : undefined; - } - }; - -}).call(this); - -/*! - * Chart.js - * http://chartjs.org/ - * Version: 2.0.0-alpha - * - * Copyright 2015 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ - - -(function() { - - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - Chart.defaults.global.tooltips = { - enabled: true, - custom: null, - backgroundColor: "rgba(0,0,0,0.8)", - fontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - fontSize: 10, - fontStyle: "normal", - fontColor: "#fff", - titleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - titleFontSize: 12, - titleFontStyle: "bold", - titleFontColor: "#fff", - yPadding: 6, - xPadding: 6, - caretSize: 8, - cornerRadius: 6, - xOffset: 10, - template: [ - '<% if(label){ %>', - '<%=label %>: ', - '<% } %>', - '<%=value %>', - ].join(''), - multiTemplate: [ - '<%if (datasetLabel){ %>', - '<%=datasetLabel %>: ', - '<% } %>', - '<%=value %>' - ].join(''), - multiKeyBackground: '#fff', - }; - - Chart.Tooltip = Chart.Element.extend({ - initialize: function() { - var options = this._options; - helpers.extend(this, { - _model: { - // Positioning - xPadding: options.tooltips.xPadding, - yPadding: options.tooltips.yPadding, - xOffset: options.tooltips.xOffset, - - // Labels - textColor: options.tooltips.fontColor, - _fontFamily: options.tooltips.fontFamily, - _fontStyle: options.tooltips.fontStyle, - fontSize: options.tooltips.fontSize, - - // Title - titleTextColor: options.tooltips.titleFontColor, - _titleFontFamily: options.tooltips.titleFontFamily, - _titleFontStyle: options.tooltips.titleFontStyle, - titleFontSize: options.tooltips.titleFontSize, - - // Appearance - caretHeight: options.tooltips.caretSize, - cornerRadius: options.tooltips.cornerRadius, - backgroundColor: options.tooltips.backgroundColor, - opacity: 0, - legendColorBackground: options.tooltips.multiKeyBackground, - }, - }); - }, - update: function() { - - var ctx = this._chart.ctx; - - switch (this._options.hover.mode) { - case 'single': - helpers.extend(this._model, { - text: helpers.template(this._options.tooltips.template, { - // These variables are available in the template function. Add others here - element: this._active[0], - value: this._data.datasets[this._active[0]._datasetIndex].data[this._active[0]._index], - label: this._data.labels ? this._data.labels[this._active[0]._index] : '', - }), - }); - - var tooltipPosition = this._active[0].tooltipPosition(); - helpers.extend(this._model, { - x: Math.round(tooltipPosition.x), - y: Math.round(tooltipPosition.y), - caretPadding: tooltipPosition.padding - }); - - break; - - case 'label': - - // Tooltip Content - - var dataArray, - dataIndex; - - var labels = [], - colors = []; - - for (var i = this._data.datasets.length - 1; i >= 0; i--) { - dataArray = this._data.datasets[i].metaData; - dataIndex = helpers.indexOf(dataArray, this._active[0]); - if (dataIndex !== -1) { - break; - } - } - - var medianPosition = (function(index) { - // Get all the points at that particular index - var elements = [], - dataCollection, - xPositions = [], - yPositions = [], - xMax, - yMax, - xMin, - yMin; - helpers.each(this._data.datasets, function(dataset) { - dataCollection = dataset.metaData; - if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()) { - elements.push(dataCollection[dataIndex]); - } - }, this); - - // Reverse labels if stacked - helpers.each(this._options.stacked ? elements.reverse() : elements, function(element) { - xPositions.push(element._view.x); - yPositions.push(element._view.y); - - //Include any colour information about the element - labels.push(helpers.template(this._options.tooltips.multiTemplate, { - // These variables are available in the template function. Add others here - element: element, - datasetLabel: this._data.datasets[element._datasetIndex].label, - value: this._data.datasets[element._datasetIndex].data[element._index], - })); - colors.push({ - fill: element._view.backgroundColor, - stroke: element._view.borderColor - }); - - }, this); - - yMin = helpers.min(yPositions); - yMax = helpers.max(yPositions); - - xMin = helpers.min(xPositions); - xMax = helpers.max(xPositions); - - return { - x: (xMin > this._chart.width / 2) ? xMin : xMax, - y: (yMin + yMax) / 2, - }; - }).call(this, dataIndex); - - // Apply for now - helpers.extend(this._model, { - x: medianPosition.x, - y: medianPosition.y, - labels: labels, - title: this._data.labels && this._data.labels.length ? this._data.labels[this._active[0]._index] : '', - legendColors: colors, - legendBackgroundColor: this._options.tooltips.multiKeyBackground, - }); - - - // Calculate Appearance Tweaks - - this._model.height = (labels.length * this._model.fontSize) + ((labels.length - 1) * (this._model.fontSize / 2)) + (this._model.yPadding * 2) + this._model.titleFontSize * 1.5; - - var titleWidth = ctx.measureText(this.title).width, - //Label has a legend square as well so account for this. - labelWidth = helpers.longestText(ctx, this.font, labels) + this._model.fontSize + 3, - longestTextWidth = helpers.max([labelWidth, titleWidth]); - - this._model.width = longestTextWidth + (this._model.xPadding * 2); - - - var halfHeight = this._model.height / 2; - - //Check to ensure the height will fit on the canvas - if (this._model.y - halfHeight < 0) { - this._model.y = halfHeight; - } else if (this._model.y + halfHeight > this._chart.height) { - this._model.y = this._chart.height - halfHeight; - } - - //Decide whether to align left or right based on position on canvas - if (this._model.x > this._chart.width / 2) { - this._model.x -= this._model.xOffset + this._model.width; - } else { - this._model.x += this._model.xOffset; - } - break; - } - - return this; - }, - draw: function() { - - var ctx = this._chart.ctx; - var vm = this._view; - - switch (this._options.hover.mode) { - case 'single': - - ctx.font = helpers.fontString(vm.fontSize, vm._fontStyle, vm._fontFamily); - - vm.xAlign = "center"; - vm.yAlign = "above"; - - //Distance between the actual element.y position and the start of the tooltip caret - var caretPadding = vm.caretPadding || 2; - - var tooltipWidth = ctx.measureText(vm.text).width + 2 * vm.xPadding, - tooltipRectHeight = vm.fontSize + 2 * vm.yPadding, - tooltipHeight = tooltipRectHeight + vm.caretHeight + caretPadding; - - if (vm.x + tooltipWidth / 2 > this._chart.width) { - vm.xAlign = "left"; - } else if (vm.x - tooltipWidth / 2 < 0) { - vm.xAlign = "right"; - } - - if (vm.y - tooltipHeight < 0) { - vm.yAlign = "below"; - } - - var tooltipX = vm.x - tooltipWidth / 2, - tooltipY = vm.y - tooltipHeight; - - ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); - - // Custom Tooltips - if (this._custom) { - this._custom(this._view); - } else { - switch (vm.yAlign) { - case "above": - //Draw a caret above the x/y - ctx.beginPath(); - ctx.moveTo(vm.x, vm.y - caretPadding); - ctx.lineTo(vm.x + vm.caretHeight, vm.y - (caretPadding + vm.caretHeight)); - ctx.lineTo(vm.x - vm.caretHeight, vm.y - (caretPadding + vm.caretHeight)); - ctx.closePath(); - ctx.fill(); - break; - case "below": - tooltipY = vm.y + caretPadding + vm.caretHeight; - //Draw a caret below the x/y - ctx.beginPath(); - ctx.moveTo(vm.x, vm.y + caretPadding); - ctx.lineTo(vm.x + vm.caretHeight, vm.y + caretPadding + vm.caretHeight); - ctx.lineTo(vm.x - vm.caretHeight, vm.y + caretPadding + vm.caretHeight); - ctx.closePath(); - ctx.fill(); - break; - } - - switch (vm.xAlign) { - case "left": - tooltipX = vm.x - tooltipWidth + (vm.cornerRadius + vm.caretHeight); - break; - case "right": - tooltipX = vm.x - (vm.cornerRadius + vm.caretHeight); - break; - } - - helpers.drawRoundedRectangle(ctx, tooltipX, tooltipY, tooltipWidth, tooltipRectHeight, vm.cornerRadius); - - ctx.fill(); - - ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString(); - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(vm.text, tooltipX + tooltipWidth / 2, tooltipY + tooltipRectHeight / 2); - - } - break; - case 'label': - - helpers.drawRoundedRectangle(ctx, vm.x, vm.y - vm.height / 2, vm.width, vm.height, vm.cornerRadius); - ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); - ctx.fill(); - ctx.closePath(); - - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - ctx.fillStyle = helpers.color(vm.titleTextColor).alpha(vm.opacity).rgbString(); - ctx.font = helpers.fontString(vm.fontSize, vm._titleFontStyle, vm._titleFontFamily); - ctx.fillText(vm.title, vm.x + vm.xPadding, this.getLineHeight(0)); - - ctx.font = helpers.fontString(vm.fontSize, vm._fontStyle, vm._fontFamily); - helpers.each(vm.labels, function(label, index) { - ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString(); - ctx.fillText(label, vm.x + vm.xPadding + vm.fontSize + 3, this.getLineHeight(index + 1)); - - //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) - //ctx.clearRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize/2, vm.fontSize, vm.fontSize); - //Instead we'll make a white filled block to put the legendColour palette over. - - ctx.fillStyle = helpers.color(vm.legendColors[index].stroke).alpha(vm.opacity).rgbString(); - ctx.fillRect(vm.x + vm.xPadding - 1, this.getLineHeight(index + 1) - vm.fontSize / 2 - 1, vm.fontSize + 2, vm.fontSize + 2); - - ctx.fillStyle = helpers.color(vm.legendColors[index].fill).alpha(vm.opacity).rgbString(); - ctx.fillRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize / 2, vm.fontSize, vm.fontSize); - - - }, this); - break; - } - }, - getLineHeight: function(index) { - var baseLineHeight = this._view.y - (this._view.height / 2) + this._view.yPadding, - afterTitleIndex = index - 1; - - //If the index is zero, we're getting the title - if (index === 0) { - return baseLineHeight + this._view.titleFontSize / 2; - } else { - return baseLineHeight + ((this._view.fontSize * 1.5 * afterTitleIndex) + this._view.fontSize / 2) + this._view.titleFontSize * 1.5; - } - - }, - }); + + "use strict"; + + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart; + + //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 < loopable.length; i++) { + callback.apply(self, [loopable[i], i].concat(additionalArgs)); + } + } else { + for (var item in loopable) { + callback.apply(self, [loopable[item], item].concat(additionalArgs)); + } + } + } + }, + clone = helpers.clone = function(obj) { + var objClone = {}; + each(obj, function(value, key) { + if (obj.hasOwnProperty(key)) { + if (helpers.isArray(value)) { + objClone[key] = value.slice(0); + } else if (typeof value === 'object' && value !== null) { + objClone[key] = clone(value); + } else { + 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); + }, + // Need a special merge function to chart configs since they are now grouped + configMerge = helpers.configMerge = function(_base) { + var base = clone(_base); + helpers.each(Array.prototype.slice.call(arguments, 1), function(extension) { + helpers.each(extension, function(value, key) { + if (extension.hasOwnProperty(key)) { + if (key === 'scales') { + // Scale config merging is complex. Add out own function here for that + base[key] = helpers.scaleMerge(base.hasOwnProperty(key) ? base[key] : {}, value); + + } else if (key === 'scale') { + // Used in polar area & radar charts since there is only one scale + base[key] = helpers.configMerge(base.hasOwnProperty(key) ? base[key] : {}, Chart.scaleService.getScaleDefaults(value.type), value); + } else if (base.hasOwnProperty(key) && helpers.isArray(base[key]) && helpers.isArray(value)) { + // In this case we have an array of objects replacing another array. Rather than doing a strict replace, + // merge. This allows easy scale option merging + var baseArray = base[key]; + + helpers.each(value, function(valueObj, index) { + + if (index < baseArray.length) { + baseArray[index] = helpers.configMerge(baseArray[index], valueObj); + } else { + baseArray.push(valueObj); // nothing to merge + } + }); + + } else if (base.hasOwnProperty(key) && typeof base[key] == "object" && base[key] !== null && typeof value == "object") { + // If we are overwriting an object with an object, do a merge of the properties. + base[key] = helpers.configMerge(base[key], value); + + } else { + // can just overwrite the value in this case + base[key] = value; + } + } + }); + }); + + return base; + }, + scaleMerge = helpers.scaleMerge = function(_base, extension) { + var base = clone(_base); + + helpers.each(extension, function(value, key) { + if (extension.hasOwnProperty(key)) { + if (key === 'xAxes' || key === 'yAxes') { + // These properties are arrays of items + if (base.hasOwnProperty(key)) { + helpers.each(value, function(valueObj, index) { + if (index >= base[key].length || !base[key][index].type) { + base[key].push(helpers.configMerge(valueObj.type ? Chart.scaleService.getScaleDefaults(valueObj.type) : {}, valueObj)); + } else if (valueObj.type !== base[key][index].type) { + // Type changed. Bring in the new defaults before we bring in valueObj so that valueObj can override the correct scale defaults + base[key][index] = helpers.configMerge(base[key][index], valueObj.type ? Chart.scaleService.getScaleDefaults(valueObj.type) : {}, valueObj) + } else { + // Type is the same + base[key][index] = helpers.configMerge(base[key][index], valueObj); + } + }); + } else { + base[key] = []; + helpers.each(value, function(valueObj) { + base[key].push(helpers.configMerge(valueObj.type ? Chart.scaleService.getScaleDefaults(valueObj.type) : {}, valueObj)); + }); + } + } else if (base.hasOwnProperty(key) && typeof base[key] == "object" && base[key] !== null && typeof value == "object") { + // If we are overwriting an object with an object, do a merge of the properties. + base[key] = helpers.configMerge(base[key], value); + + } else { + // can just overwrite the value in this case + base[key] = value; + } + } + }); + + return base; + }, + getValueAtIndexOrDefault = helpers.getValueAtIndexOrDefault = function(value, index, defaultValue) { + if (!value) { + return defaultValue; + } + + if (helpers.isArray(value) && index < value.length) { + return value[index]; + } + + return value; + }, + indexOf = helpers.indexOf = function(arrayToSearch, item) { + if (Array.prototype.indexOf) { + return arrayToSearch.indexOf(item); + } else { + for (var i = 0; i < arrayToSearch.length; i++) { + 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); + }; + + var Surrogate = function() { + this.constructor = ChartElement; + }; + Surrogate.prototype = parent.prototype; + ChartElement.prototype = new Surrogate(); + + ChartElement.extend = inherits; + + if (extensions) extend(ChartElement.prototype, extensions); + + ChartElement.__super__ = parent.prototype; + + return ChartElement; + }, + noop = helpers.noop = function() {}, + uid = helpers.uid = (function() { + var id = 0; + return function() { + return "chart-" + id++; + }; + })(), + warn = helpers.warn = function(str) { + //Method for warning of errors + if (window.console && typeof window.console.warn === "function") console.warn(str); + }, + 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) { + return Math.max.apply(Math, array); + }, + min = helpers.min = function(array) { + return Math.min.apply(Math, array); + }, + sign = helpers.sign = function(x) { + if (Math.sign) { + return Math.sign(x); + } else { + x = +x; // convert to a number + if (x === 0 || isNaN(x)) { + return x; + } + return x > 0 ? 1 : -1; + } + }, + log10 = helpers.log10 = function(x) { + if (Math.log10) { + return Math.log10(x) + } else { + return Math.log(x) / Math.LN10; + } + }, + cap = helpers.cap = function(valueToCap, maxValue, minValue) { + if (isNumber(maxValue)) { + if (valueToCap > 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)) { + 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; + } + }, + toRadians = helpers.toRadians = function(degrees) { + return degrees * (Math.PI / 180); + }, + toDegrees = helpers.toDegrees = function(radians) { + return radians * (180 / Math.PI); + }, + // 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.atan2(distanceFromYCenter, distanceFromXCenter); + + if (angle < (-0.5 * Math.PI)) { + angle += 2.0 * Math.PI; // make sure the returned angle is in the range of (-PI/2, 3PI/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 { + previous: { + x: MiddlePoint.x - fa * (AfterPoint.x - FirstPoint.x), + y: MiddlePoint.y - fa * (AfterPoint.y - FirstPoint.y) + }, + next: { + x: MiddlePoint.x + fb * (AfterPoint.x - FirstPoint.x), + y: MiddlePoint.y + fb * (AfterPoint.y - FirstPoint.y) + } + }; + }, + nextItem = helpers.nextItem = function(collection, index, loop) { + if (loop) { + return collection[index + 1] || collection[0]; + } + return collection[index + 1] || collection[collection.length - 1]; + }, + previousItem = helpers.previousItem = function(collection, index, loop) { + if (loop) { + return collection[index - 1] || collection[collection.length - 1]; + } + return collection[index - 1] || collection[0]; + }, + 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) + }; + + }, + // Implementation of the nice number algorithm used in determining where axis labels will go + niceNum = helpers.niceNum = function(range, round) { + var exponent = Math.floor(helpers.log10(range)); + var fraction = range / Math.pow(10, exponent); + var niceFraction; + + if (round) { + if (fraction < 1.5) { + niceFraction = 1; + } else if (fraction < 3) { + niceFraction = 2; + } else if (fraction < 7) { + niceFraction = 5; + } else { + niceFraction = 10; + } + } else { + if (fraction <= 1.0) { + niceFraction = 1; + } else if (fraction <= 2) { + niceFraction = 2; + } else if (fraction <= 5) { + niceFraction = 5; + } else { + niceFraction = 10; + } + } + + return niceFraction * Math.pow(10, exponent); + }, + /* 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) { + return 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 (templateString) { + 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); + }); + }, + getMaximumWidth = helpers.getMaximumWidth = function(domNode) { + 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 - padding; + }, + getMaximumHeight = helpers.getMaximumHeight = function(domNode) { + 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 - 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; + + 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(); + }, + color = helpers.color = function(color) { + if (!window.Color) { + console.log('Color.js not found!'); + return color; + } + return window.Color(color); + }, + isArray = helpers.isArray = function(obj) { + if (!Array.isArray) { + return Object.prototype.toString.call(arg) === '[object Array]'; + } + return Array.isArray(obj); + }; }).call(this); (function() { - "use strict"; - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; + "use strict"; - var DatasetScale = Chart.Element.extend({ - // overridden in the chart. Will set min and max as properties of the scale for later use. Min will always be 0 when using a dataset and max will be the number of items in the dataset - calculateRange: helpers.noop, - isHorizontal: function() { - return this.options.position == "top" || this.options.position == "bottom"; - }, - getPixelForValue: function(value, index, includeOffset) { - // This must be called after fit has been run so that - // this.left, this.top, this.right, and this.bottom have been defined - if (this.isHorizontal()) { - var isRotated = (this.labelRotation > 0); - var innerWidth = this.width - (this.paddingLeft + this.paddingRight); - var valueWidth = innerWidth / Math.max((this.max - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1); - var valueOffset = (valueWidth * index) + this.paddingLeft; + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart, + helpers = Chart.helpers; - if (this.options.gridLines.offsetGridLines && includeOffset) { - valueOffset += (valueWidth / 2); - } + Chart.elements = {}; - return this.left + Math.round(valueOffset); - } else { - return this.top + (index * (this.height / this.max)); - } - }, - calculateLabelRotation: function(maxHeight, margins) { - //Get the width of each grid by calculating the difference - //between x offsets between 0 and 1. - var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); - this.ctx.font = labelFont; + Chart.Element = function(configuration) { + helpers.extend(this, configuration); + this.initialize.apply(this, arguments); + }; + helpers.extend(Chart.Element.prototype, { + initialize: function() {}, + pivot: function() { + if (!this._view) { + this._view = helpers.clone(this._model); + } + this._start = helpers.clone(this._view); + return this; + }, + transition: function(ease) { + if (!this._view) { + this._view = helpers.clone(this._model); + } + if (!this._start) { + this.pivot(); + } - var firstWidth = this.ctx.measureText(this.labels[0]).width; - var lastWidth = this.ctx.measureText(this.labels[this.labels.length - 1]).width; - var firstRotated; - var lastRotated; + helpers.each(this._model, function(value, key) { - this.paddingRight = lastWidth / 2 + 3; - this.paddingLeft = firstWidth / 2 + 3; + if (key[0] === '_' || !this._model.hasOwnProperty(key)) { + // Only non-underscored properties + } - this.labelRotation = 0; + // Init if doesn't exist + else if (!this._view[key]) { + if (typeof value === 'number') { + this._view[key] = value * ease; + } else { + this._view[key] = value || null; + } + } - if (this.options.display) { - var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels); - var cosRotation; - var sinRotation; - var firstRotatedWidth; + // No unnecessary computations + else if (this._model[key] === this._view[key]) { + // It's the same! Woohoo! + } - this.labelWidth = originalLabelWidth; + // Color transitions if possible + else if (typeof value === 'string') { + try { + var color = helpers.color(this._start[key]).mix(helpers.color(this._model[key]), ease); + this._view[key] = color.rgbString(); + } catch (err) { + this._view[key] = value; + } + } + // Number transitions + else if (typeof value === 'number') { + var startVal = this._start[key] !== undefined ? this._start[key] : 0; + this._view[key] = ((this._model[key] - startVal) * ease) + startVal; + } + // Everything else + else { + this._view[key] = value; + } - //Allow 3 pixels x2 padding either side for label readability - // only the index matters for a dataset scale, but we want a consistent interface between scales - var gridWidth = Math.floor(this.getPixelForValue(0, 1) - this.getPixelForValue(0, 0)) - 6; + }, this); - //Max label rotate should be 90 - also act as a loop counter - while ((this.labelWidth > gridWidth && this.labelRotation === 0) || (this.labelWidth > gridWidth && this.labelRotation <= 90 && this.labelRotation > 0)) { - cosRotation = Math.cos(helpers.toRadians(this.labelRotation)); - sinRotation = Math.sin(helpers.toRadians(this.labelRotation)); + if (ease === 1) { + delete this._start; + } + return this; + }, + tooltipPosition: function() { + return { + x: this._model.x, + y: this._model.y + }; + }, + hasValue: function() { + return helpers.isNumber(this._model.x) && helpers.isNumber(this._model.y); + } + }); - firstRotated = cosRotation * firstWidth; - lastRotated = cosRotation * lastWidth; + Chart.Element.extend = helpers.inherits; - // We're right aligning the text now. - if (firstRotated + this.options.labels.fontSize / 2 > this.yLabelWidth) { - this.paddingLeft = firstRotated + this.options.labels.fontSize / 2; - } +}).call(this); - this.paddingRight = this.options.labels.fontSize / 2; +(function() { - if (sinRotation * originalLabelWidth > maxHeight) { - // go back one step - this.labelRotation--; - break; - } + "use strict"; - this.labelRotation++; - this.labelWidth = cosRotation * originalLabelWidth; + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; - } - } else { - this.labelWidth = 0; - this.paddingRight = 0; - this.paddingLeft = 0; - } + Chart.defaults.global.animation = { + duration: 1000, + easing: "easeOutQuart", + onProgress: function() {}, + onComplete: function() {}, + }; - if (margins) { - this.paddingLeft -= margins.left; - this.paddingRight -= margins.right; + 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 - this.paddingLeft = Math.max(this.paddingLeft, 0); - this.paddingRight = Math.max(this.paddingRight, 0); - } + onAnimationProgress: null, // user specified callback to fire on each step of the animation + onAnimationComplete: null, // user specified callback to fire when the animation finishes + }); - }, - // Fit this axis to the given size - // @param {number} maxWidth : the max width the axis can be - // @param {number} maxHeight: the max height the axis can be - // @return {object} minSize : the minimum size needed to draw the axis - fit: function(maxWidth, maxHeight, margins) { - this.calculateRange(); - this.calculateLabelRotation(maxHeight, margins); + Chart.animationService = { + frameDuration: 17, + animations: [], + dropFrames: 0, + addAnimation: function(chartInstance, animationObject, duration) { - var minSize = { - width: 0, - height: 0, - }; + if (!duration) { + chartInstance.animating = true; + } - var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); - var longestLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels); + 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; + } + } - // Width - if (this.isHorizontal()) { - minSize.width = maxWidth; - this.width = maxWidth; - } else if (this.options.display) { - minSize.width = Math.min(longestLabelWidth + 6, maxWidth); - } + this.animations.push({ + chartInstance: chartInstance, + animationObject: animationObject + }); - // Height - if (this.isHorizontal()) { - var labelHeight = (Math.cos(helpers.toRadians(this.labelRotation)) * longestLabelWidth) + 1.5 * this.options.labels.fontSize; - minSize.height = Math.min(labelHeight, maxHeight); - } else if (this.options.display) { - minSize.width = Math.min(longestLabelWidth + 6, maxWidth); - } + // 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; + }); - this.width = minSize.width; - this.height = minSize.height; - return minSize; - }, - // Actualy draw the scale on the canvas - // @param {rectangle} chartArea : the area of the chart to draw full grid lines on - draw: function(chartArea) { - if (this.options.display) { + if (index) { + this.animations.splice(index, 1); + chartInstance.animating = false; + } + }, + // calls startDigest with the proper context + digestWrapper: function() { + Chart.animationService.startDigest.call(Chart.animationService); + }, + startDigest: function() { - var setContextLineSettings; + var startTime = Date.now(); + var framesToDrop = 0; - // Make sure we draw text in the correct color - this.ctx.fillStyle = this.options.labels.fontColor; + if (this.dropFrames > 1) { + framesToDrop = Math.floor(this.dropFrames); + this.dropFrames -= framesToDrop; + } - if (this.isHorizontal()) { - setContextLineSettings = true; - var yTickStart = this.options.position == "bottom" ? this.top : this.bottom - 10; - var yTickEnd = this.options.position == "bottom" ? this.top + 10 : this.bottom; - var isRotated = this.labelRotation !== 0; + for (var i = 0; i < this.animations.length; i++) { - helpers.each(this.labels, function(label, index) { - var xLineValue = this.getPixelForValue(label, index, false); // xvalues for grid lines - var xLabelValue = this.getPixelForValue(label, index, true); // x values for labels (need to consider offsetLabel option) + if (this.animations[i].animationObject.currentStep === null) { + this.animations[i].animationObject.currentStep = 0; + } - if (this.options.gridLines.show) { - if (index === 0) { - // Draw the first index specially - this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; - this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; - setContextLineSettings = true; // reset next time - } else if (setContextLineSettings) { - this.ctx.lineWidth = this.options.gridLines.lineWidth; - this.ctx.strokeStyle = this.options.gridLines.color; - setContextLineSettings = false; - } + 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; + } - xLineValue += helpers.aliasPixel(this.ctx.lineWidth); + this.animations[i].animationObject.render(this.animations[i].chartInstance, this.animations[i].animationObject); - // Draw the label area - this.ctx.beginPath(); + if (this.animations[i].animationObject.currentStep == this.animations[i].animationObject.numSteps) { + // executed the last frame. Remove the animation. + this.animations[i].chartInstance.animating = false; + this.animations.splice(i, 1); + // Keep the index in place to offset the splice + i--; + } + } - if (this.options.gridLines.drawTicks) { - this.ctx.moveTo(xLineValue, yTickStart); - this.ctx.lineTo(xLineValue, yTickEnd); - } + var endTime = Date.now(); + var delay = endTime - startTime - this.frameDuration; + var frameDelay = delay / this.frameDuration; - // Draw the chart area - if (this.options.gridLines.drawOnChartArea) { - this.ctx.moveTo(xLineValue, chartArea.top); - this.ctx.lineTo(xLineValue, chartArea.bottom); - } + if (frameDelay > 1) { + this.dropFrames += frameDelay; + } - // Need to stroke in the loop because we are potentially changing line widths & colours - this.ctx.stroke(); - } + // Do we have more stuff to animate? + if (this.animations.length > 0) { + helpers.requestAnimFrame.call(window, this.digestWrapper); + } + } + }; - if (this.options.labels.show) { - this.ctx.save(); - this.ctx.translate(xLabelValue, (isRotated) ? this.top + 12 : this.top + 8); - this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1); - this.ctx.font = this.font; - this.ctx.textAlign = (isRotated) ? "right" : "center"; - this.ctx.textBaseline = (isRotated) ? "middle" : "top"; - this.ctx.fillText(label, 0, 0); - this.ctx.restore(); - } - }, this); - } else { - // Vertical - if (this.options.gridLines.show) {} +}).call(this); - if (this.options.labels.show) { - // Draw the labels - } - } - } - } - }); - Chart.scales.registerScaleType("category", DatasetScale); +(function() { + + "use strict"; + + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart, + helpers = Chart.helpers; + + + //Create a dictionary of chart types, to allow for extension of existing types + Chart.types = {}; + + //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 = {}; + + // Controllers available for dataset visualization eg. bar, line, slice, etc. + Chart.controllers = {}; + + // The main controller of a chart + Chart.Controller = function(instance) { + + this.chart = instance; + this.config = instance.config; + this.data = this.config.data; + this.options = this.config.options = helpers.configMerge(Chart.defaults.global, Chart.defaults[this.config.type], this.config.options || {}); + this.id = helpers.uid(); + + //Add the chart instance to the global namespace + Chart.instances[this.id] = this; + + if (this.options.responsive) { + this.resize(); + } + + this.initialize.call(this); + + return this; + }; + + helpers.extend(Chart.Controller.prototype, { + + initialize: function initialize() { + + // TODO + // If BeforeInit(this) doesn't return false, proceed + + this.bindEvents(); + this.buildScales(); + this.buildControllers(); + this.resetElements(); + this.initToolTip(); + this.update(); + + // TODO + // If AfterInit(this) doesn't return false, proceed + + return this; + }, + + clear: function clear() { + helpers.clear(this.chart); + return this; + }, + + stop: function stop() { + // Stops any current animation loop occuring + Chart.animationService.cancelAnimation(this); + return this; + }, + + resize: function resize() { + this.stop(); + var canvas = this.chart.canvas, + newWidth = helpers.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; + + helpers.retinaScale(this.chart); + + return this; + }, + + buildScales: function buildScales() { + // Map of scale ID to scale object so we can lookup later + this.scales = {}; + + // Build the x axes + helpers.each(this.options.scales.xAxes, function(xAxisOptions) { + var ScaleClass = Chart.scaleService.getScaleConstructor(xAxisOptions.type); + var scale = new ScaleClass({ + ctx: this.chart.ctx, + options: xAxisOptions, + data: this.data, + id: xAxisOptions.id, + }); + + this.scales[scale.id] = scale; + }, this); + + // Build the y axes + helpers.each(this.options.scales.yAxes, function(yAxisOptions) { + var ScaleClass = Chart.scaleService.getScaleConstructor(yAxisOptions.type); + var scale = new ScaleClass({ + ctx: this.chart.ctx, + options: yAxisOptions, + data: this.data, + id: yAxisOptions.id, + }); + + this.scales[scale.id] = scale; + }, this); + + Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); + }, + + buildControllers: function() { + this.eachDataset(function(dataset, datasetIndex) { + var type = dataset.type || this.config.type; + if (dataset.controller) { + dataset.controller.updateIndex(datasetIndex); + return; + } + dataset.controller = new Chart.controllers[type](this, datasetIndex); + }); + }, + + resetElements: function resetElements() { + this.eachDataset(function(dataset, datasetIndex) { + dataset.controller.reset(); + }); + }, + + + update: function update(animationDuration) { + // This will loop through any data and do the appropriate element update for the type + Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); + this.eachDataset(function(dataset, datasetIndex) { + dataset.controller.update(); + }); + this.render(animationDuration); + }, + + render: function render(duration) { + + if (this.options.animation.duration !== 0 || duration) { + var animation = new Chart.Animation(); + animation.numSteps = (duration || this.options.animation.duration) / 16.66; //60 fps + animation.easing = this.options.animation.easing; + + // 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, duration); + } else { + this.draw(); + this.options.onAnimationComplete.call(this); + } + return this; + }, + + draw: function(ease) { + var easingDecimal = ease || 1; + this.clear(); + + // Draw all the scales + helpers.each(this.scales, function(scale) { + scale.draw(this.chartArea); + }, this); + + // Draw each dataset via its respective controller + // TODO: needs support for reverse stacking (line chart) + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + dataset.controller.draw(ease); + }, this); + + // Finally draw the tooltip + this.tooltip.transition(easingDecimal).draw(); + }, + + + + + + + + eachValue: function eachValue(callback) { + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + helpers.each(dataset.data, callback, this, datasetIndex); + }, this); + }, + + eachElement: function eachElement(callback) { + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + helpers.each(dataset.metaData, callback, this, dataset.metaData, datasetIndex); + }, this); + }, + + eachDataset: function eachDataset(callback) { + helpers.each(this.data.datasets, callback, this); + }, + + // 2 helper functions to get next/previous elements in datasets + nextElement: function nextElement(datasets, index, loop) { + if (this.loop) { + return dataset[index + 1] || dataset[0]; + } + return datasets[index + 1] || datasets[index]; + }, + previousElement: function previousElement(datasets, index, loop) { + if (this.loop) { + return dataset[index - 1] || dataset[dataset.length - 1]; + } + return datasets[index - 1] || datasets[index]; + }, + + // Get the single element that was clicked on + // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was draw + getElementAtEvent: function getElementAtEvent(e) { + + var element = []; + var eventPosition = helpers.getRelativePosition(e); + + for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; ++datasetIndex) { + for (var elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; ++elementIndex) { + if (this.data.datasets[datasetIndex].metaData[elementIndex].inRange(eventPosition.x, eventPosition.y)) { + element.push(this.data.datasets[datasetIndex].metaData[elementIndex]); + return element; + } + } + } + + return []; + }, + + getElementsAtEvent: function getElementsAtEvent(e) { + + var elementsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset) { + elementsArray.push(dataset.metaData[elementIndex]); + }, + elementIndex; + + for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; datasetIndex++) { + for (elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; elementIndex++) { + if (this.data.datasets[datasetIndex].metaData[elementIndex].inGroupRange(eventPosition.x, eventPosition.y)) { + helpers.each(this.data.datasets, datasetIterator); + } + } + } + + return elementsArray.length ? elementsArray : []; + }, + + getDatasetAtEvent: function getDatasetAtEvent(e) { + + var elementsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset) { + elementsArray.push(dataset.metaData[elementIndex]); + }, + elementIndex; + + for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; datasetIndex++) { + for (elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; elementIndex++) { + if (this.data.datasets[datasetIndex].metaData[elementIndex].inGroupRange(eventPosition.x, eventPosition.y)) { + helpers.each(this.data.datasets, datasetIterator); + } + } + } + + return elementsArray.length ? elementsArray : []; + }, + + generateLegend: function generateLegend() { + return template(this.options.legendTemplate, this); + }, + + destroy: function destroy() { + this.clear(); + helpers.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]; + }, + + toBase64Image: function toBase64Image() { + return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); + }, + + initToolTip: function initToolTip() { + this.tooltip = new Chart.Tooltip({ + _chart: this.chart, + _data: this.data, + _options: this.options, + }, this); + }, + + bindEvents: function bindEvents() { + helpers.bindEvents(this, this.options.events, function(evt) { + this.eventHandler(evt); + }); + }, + eventHandler: function eventHandler(e) { + this.lastActive = this.lastActive || []; + + // Find Active Elements + if (e.type == 'mouseout') { + this.active = []; + } else { + this.active = function() { + switch (this.options.hover.mode) { + case 'single': + return this.elementController.getElementAtEvent(e); + case 'label': + return this.elementController.getElementsAtEvent(e); + case 'dataset': + return this.elementController.getDatasetAtEvent(e); + default: + return e; + } + }.call(this); + } + + // On Hover hook + if (this.options.hover.onHover) { + this.options.hover.onHover.call(this, this.active); + } + + if (e.type == 'mouseup' || e.type == 'click') { + if (this.options.onClick) { + this.options.onClick.call(this, e, this.active); + } + } + + var dataset; + var index; + // Remove styling for last active (even if it may still be active) + if (this.lastActive.length) { + switch (this.options.hover.mode) { + case 'single': + this.elementController.resetElementAppearance(this.lastActive[0], this.lastActive[0]._datasetIndex, this.lastActive[0]._index); + break; + case 'label': + for (var i = 0; i < this.lastActive.length; i++) { + this.elementController.resetElementAppearance(this.lastActive[i], this.lastActive[i]._datasetIndex, this.lastActive[i]._index); + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + // Built in hover styling + if (this.active.length && this.options.hover.mode) { + switch (this.options.hover.mode) { + case 'single': + this.elementController.setElementHoverStyle(this.active[0]); + break; + case 'label': + for (var i = 0; i < this.active.length; i++) { + this.elementController.setElementHoverStyle(this.active[i]); + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + + // Built in Tooltips + if (this.options.tooltips.enabled) { + + // The usual updates + this.tooltip.initialize(); + + // Active + if (this.active.length) { + this.tooltip._model.opacity = 1; + + helpers.extend(this.tooltip, { + _active: this.active, + }); + + this.tooltip.update(); + } else { + // Inactive + this.tooltip._model.opacity = 0; + } + } + + // Hover animations + this.tooltip.pivot(); + + if (!this.animating) { + var changed; + + helpers.each(this.active, function(element, index) { + if (element !== this.lastActive[index]) { + changed = true; + } + }, this); + + // If entering, leaving, or changing elements, animate the change via pivot + if ((!this.lastActive.length && this.active.length) || + (this.lastActive.length && !this.active.length) || + (this.lastActive.length && this.active.length && changed)) { + + this.stop(); + this.render(this.options.hover.animationDuration); + } + } + + // Remember Last Active + this.lastActive = this.active; + return this; + }, + }); + +}).call(this); + +(function() { + + "use strict"; + + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart, + helpers = Chart.helpers; + + + // 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.update(); + } + }); + }, 50); + }; + })()); + +}).call(this); + +(function() { + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + // The scale service is used to resize charts along with all of their axes. We make this as + // a service where scales are registered with their respective charts so that changing the + // scales does not require + Chart.scaleService = { + // Scale registration object. Extensions can register new scale types (such as log or DB scales) and then + // use the new chart options to grab the correct scale + constructors: {}, + // Use a registration function so that we can move to an ES6 map when we no longer need to support + // old browsers + // Scale config defaults + defaults: {}, + registerScaleType: function(type, scaleConstructor, defaults) { + this.constructors[type] = scaleConstructor; + this.defaults[type] = defaults; + }, + getScaleConstructor: function(type) { + return this.constructors.hasOwnProperty(type) ? this.constructors[type] : undefined; + }, + getScaleDefaults: function(type) { + return this.defaults.hasOwnProperty(type) ? this.defaults[type] : {}; + }, + // The interesting function + fitScalesForChart: function(chartInstance, width, height) { + var xPadding = width > 30 ? 5 : 2; + var yPadding = height > 30 ? 5 : 2; + + if (chartInstance) { + var leftScales = helpers.where(chartInstance.scales, function(scaleInstance) { + return scaleInstance.options.position == "left"; + }); + var rightScales = helpers.where(chartInstance.scales, function(scaleInstance) { + return scaleInstance.options.position == "right"; + }); + var topScales = helpers.where(chartInstance.scales, function(scaleInstance) { + return scaleInstance.options.position == "top"; + }); + var bottomScales = helpers.where(chartInstance.scales, function(scaleInstance) { + return scaleInstance.options.position == "bottom"; + }); + + // Essentially we now have any number of scales on each of the 4 sides. + // Our canvas looks like the following. + // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and + // B1 is the bottom axis + // |------------------------------------------------------| + // | | T1 | | + // |----|-----|-------------------------------------|-----| + // | | | | | + // | L1 | L2 | Chart area | R1 | + // | | | | | + // | | | | | + // |----|-----|-------------------------------------|-----| + // | | B1 | | + // | | | | + // |------------------------------------------------------| + + // What we do to find the best sizing, we do the following + // 1. Determine the minimum size of the chart area. + // 2. Split the remaining width equally between each vertical axis + // 3. Split the remaining height equally between each horizontal axis + // 4. Give each scale the maximum size it can be. The scale will return it's minimum size + // 5. Adjust the sizes of each axis based on it's minimum reported size. + // 6. Refit each axis + // 7. Position each axis in the final location + // 8. Tell the chart the final location of the chart area + + // Step 1 + var chartWidth = width / 2; // min 50% + var chartHeight = height / 2; // min 50% + + chartWidth -= (2 * xPadding); + chartHeight -= (2 * yPadding); + + + // Step 2 + var verticalScaleWidth = (width - chartWidth) / (leftScales.length + rightScales.length); + + // Step 3 + var horizontalScaleHeight = (height - chartHeight) / (topScales.length + bottomScales.length); + + // Step 4; + var minimumScaleSizes = []; + + var verticalScaleMinSizeFunction = function(scaleInstance) { + var minSize = scaleInstance.fit(verticalScaleWidth, chartHeight); + minimumScaleSizes.push({ + horizontal: false, + minSize: minSize, + scale: scaleInstance, + }); + }; + + var horizontalScaleMinSizeFunction = function(scaleInstance) { + var minSize = scaleInstance.fit(chartWidth, horizontalScaleHeight); + minimumScaleSizes.push({ + horizontal: true, + minSize: minSize, + scale: scaleInstance, + }); + }; + + // vertical scales + helpers.each(leftScales, verticalScaleMinSizeFunction); + helpers.each(rightScales, verticalScaleMinSizeFunction); + + // horizontal scales + helpers.each(topScales, horizontalScaleMinSizeFunction); + helpers.each(bottomScales, horizontalScaleMinSizeFunction); + + // Step 5 + var maxChartHeight = height - (2 * yPadding); + var maxChartWidth = width - (2 * xPadding); + + helpers.each(minimumScaleSizes, function(wrapper) { + if (wrapper.horizontal) { + maxChartHeight -= wrapper.minSize.height; + } else { + maxChartWidth -= wrapper.minSize.width; + } + }); + + // At this point, maxChartHeight and maxChartWidth are the size the chart area could + // be if the axes are drawn at their minimum sizes. + + // Step 6 + var verticalScaleFitFunction = function(scaleInstance) { + var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) { + return wrapper.scale === scaleInstance; + }); + + if (wrapper) { + scaleInstance.fit(wrapper.minSize.width, maxChartHeight); + } + }; + + var horizontalScaleFitFunction = function(scaleInstance) { + var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) { + return wrapper.scale === scaleInstance; + }); + + var scaleMargin = { + left: totalLeftWidth, + right: totalRightWidth, + top: 0, + bottom: 0, + }; + + if (wrapper) { + scaleInstance.fit(maxChartWidth, wrapper.minSize.height, scaleMargin); + } + }; + + var totalLeftWidth = xPadding; + var totalRightWidth = xPadding; + var totalTopHeight = yPadding; + var totalBottomHeight = yPadding; + + helpers.each(leftScales, verticalScaleFitFunction); + helpers.each(rightScales, verticalScaleFitFunction); + + // Figure out how much margin is on the left and right of the horizontal axes + helpers.each(leftScales, function(scaleInstance) { + totalLeftWidth += scaleInstance.width; + }); + + helpers.each(rightScales, function(scaleInstance) { + totalRightWidth += scaleInstance.width; + }); + + helpers.each(topScales, horizontalScaleFitFunction); + helpers.each(bottomScales, horizontalScaleFitFunction); + + helpers.each(topScales, function(scaleInstance) { + totalTopHeight += scaleInstance.height; + }); + helpers.each(bottomScales, function(scaleInstance) { + totalBottomHeight += scaleInstance.height; + }); + + // Let the left scale know the final margin + helpers.each(leftScales, function(scaleInstance) { + var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) { + return wrapper.scale === scaleInstance; + }); + + var scaleMargin = { + left: 0, + right: 0, + top: totalTopHeight, + bottom: totalBottomHeight + }; + + if (wrapper) { + scaleInstance.fit(wrapper.minSize.width, maxChartHeight, scaleMargin); + } + }); + + helpers.each(rightScales, function(scaleInstance) { + var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) { + return wrapper.scale === scaleInstance; + }); + + var scaleMargin = { + left: 0, + right: 0, + top: totalTopHeight, + bottom: totalBottomHeight + }; + + if (wrapper) { + scaleInstance.fit(wrapper.minSize.width, maxChartHeight, scaleMargin); + } + }); + + // Step 7 + // Position the scales + var left = xPadding; + var top = yPadding; + var right = 0; + var bottom = 0; + + var verticalScalePlacer = function(scaleInstance) { + scaleInstance.left = left; + scaleInstance.right = left + scaleInstance.width; + scaleInstance.top = totalTopHeight; + scaleInstance.bottom = totalTopHeight + maxChartHeight; + + // Move to next point + left = scaleInstance.right; + }; + + var horizontalScalePlacer = function(scaleInstance) { + scaleInstance.left = totalLeftWidth; + scaleInstance.right = totalLeftWidth + maxChartWidth; + scaleInstance.top = top; + scaleInstance.bottom = top + scaleInstance.height; + + // Move to next point + top = scaleInstance.bottom; + }; + + helpers.each(leftScales, verticalScalePlacer); + helpers.each(topScales, horizontalScalePlacer); + + // Account for chart width and height + left += maxChartWidth; + top += maxChartHeight; + + helpers.each(rightScales, verticalScalePlacer); + helpers.each(bottomScales, horizontalScalePlacer); + + // Step 8 + chartInstance.chartArea = { + left: totalLeftWidth, + top: totalTopHeight, + right: totalLeftWidth + maxChartWidth, + bottom: totalTopHeight + maxChartHeight, + }; + } + } + }; +}).call(this); + +(function() { + + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + Chart.defaults.global.tooltips = { + enabled: true, + custom: null, + backgroundColor: "rgba(0,0,0,0.8)", + fontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + fontSize: 10, + fontStyle: "normal", + fontColor: "#fff", + titleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + titleFontSize: 12, + titleFontStyle: "bold", + titleFontColor: "#fff", + yPadding: 6, + xPadding: 6, + caretSize: 8, + cornerRadius: 6, + xOffset: 10, + template: [ + '<% if(label){ %>', + '<%=label %>: ', + '<% } %>', + '<%=value %>', + ].join(''), + multiTemplate: [ + '<%if (datasetLabel){ %>', + '<%=datasetLabel %>: ', + '<% } %>', + '<%=value %>' + ].join(''), + multiKeyBackground: '#fff', + }; + + Chart.Tooltip = Chart.Element.extend({ + initialize: function() { + var options = this._options; + helpers.extend(this, { + _model: { + // Positioning + xPadding: options.tooltips.xPadding, + yPadding: options.tooltips.yPadding, + xOffset: options.tooltips.xOffset, + + // Labels + textColor: options.tooltips.fontColor, + _fontFamily: options.tooltips.fontFamily, + _fontStyle: options.tooltips.fontStyle, + fontSize: options.tooltips.fontSize, + + // Title + titleTextColor: options.tooltips.titleFontColor, + _titleFontFamily: options.tooltips.titleFontFamily, + _titleFontStyle: options.tooltips.titleFontStyle, + titleFontSize: options.tooltips.titleFontSize, + + // Appearance + caretHeight: options.tooltips.caretSize, + cornerRadius: options.tooltips.cornerRadius, + backgroundColor: options.tooltips.backgroundColor, + opacity: 0, + legendColorBackground: options.tooltips.multiKeyBackground, + }, + }); + }, + update: function() { + + var ctx = this._chart.ctx; + + switch (this._options.hover.mode) { + case 'single': + helpers.extend(this._model, { + text: helpers.template(this._options.tooltips.template, { + // These variables are available in the template function. Add others here + element: this._active[0], + value: this._data.datasets[this._active[0]._datasetIndex].data[this._active[0]._index], + label: this._data.labels ? this._data.labels[this._active[0]._index] : '', + }), + }); + + var tooltipPosition = this._active[0].tooltipPosition(); + helpers.extend(this._model, { + x: Math.round(tooltipPosition.x), + y: Math.round(tooltipPosition.y), + caretPadding: tooltipPosition.padding + }); + + break; + + case 'label': + + // Tooltip Content + + var dataArray, + dataIndex; + + var labels = [], + colors = []; + + for (var i = this._data.datasets.length - 1; i >= 0; i--) { + dataArray = this._data.datasets[i].metaData; + dataIndex = helpers.indexOf(dataArray, this._active[0]); + if (dataIndex !== -1) { + break; + } + } + + var medianPosition = (function(index) { + // Get all the points at that particular index + var elements = [], + dataCollection, + xPositions = [], + yPositions = [], + xMax, + yMax, + xMin, + yMin; + helpers.each(this._data.datasets, function(dataset) { + dataCollection = dataset.metaData; + if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()) { + elements.push(dataCollection[dataIndex]); + } + }, this); + + // Reverse labels if stacked + helpers.each(this._options.stacked ? elements.reverse() : elements, function(element) { + xPositions.push(element._view.x); + yPositions.push(element._view.y); + + //Include any colour information about the element + labels.push(helpers.template(this._options.tooltips.multiTemplate, { + // These variables are available in the template function. Add others here + element: element, + datasetLabel: this._data.datasets[element._datasetIndex].label, + value: this._data.datasets[element._datasetIndex].data[element._index], + })); + colors.push({ + fill: element._view.backgroundColor, + stroke: element._view.borderColor + }); + + }, this); + + yMin = helpers.min(yPositions); + yMax = helpers.max(yPositions); + + xMin = helpers.min(xPositions); + xMax = helpers.max(xPositions); + + return { + x: (xMin > this._chart.width / 2) ? xMin : xMax, + y: (yMin + yMax) / 2, + }; + }).call(this, dataIndex); + + // Apply for now + helpers.extend(this._model, { + x: medianPosition.x, + y: medianPosition.y, + labels: labels, + title: this._data.labels && this._data.labels.length ? this._data.labels[this._active[0]._index] : '', + legendColors: colors, + legendBackgroundColor: this._options.tooltips.multiKeyBackground, + }); + + + // Calculate Appearance Tweaks + + this._model.height = (labels.length * this._model.fontSize) + ((labels.length - 1) * (this._model.fontSize / 2)) + (this._model.yPadding * 2) + this._model.titleFontSize * 1.5; + + var titleWidth = ctx.measureText(this.title).width, + //Label has a legend square as well so account for this. + labelWidth = helpers.longestText(ctx, this.font, labels) + this._model.fontSize + 3, + longestTextWidth = helpers.max([labelWidth, titleWidth]); + + this._model.width = longestTextWidth + (this._model.xPadding * 2); + + + var halfHeight = this._model.height / 2; + + //Check to ensure the height will fit on the canvas + if (this._model.y - halfHeight < 0) { + this._model.y = halfHeight; + } else if (this._model.y + halfHeight > this._chart.height) { + this._model.y = this._chart.height - halfHeight; + } + + //Decide whether to align left or right based on position on canvas + if (this._model.x > this._chart.width / 2) { + this._model.x -= this._model.xOffset + this._model.width; + } else { + this._model.x += this._model.xOffset; + } + break; + } + + return this; + }, + draw: function() { + + var ctx = this._chart.ctx; + var vm = this._view; + + switch (this._options.hover.mode) { + case 'single': + + ctx.font = helpers.fontString(vm.fontSize, vm._fontStyle, vm._fontFamily); + + vm.xAlign = "center"; + vm.yAlign = "above"; + + //Distance between the actual element.y position and the start of the tooltip caret + var caretPadding = vm.caretPadding || 2; + + var tooltipWidth = ctx.measureText(vm.text).width + 2 * vm.xPadding, + tooltipRectHeight = vm.fontSize + 2 * vm.yPadding, + tooltipHeight = tooltipRectHeight + vm.caretHeight + caretPadding; + + if (vm.x + tooltipWidth / 2 > this._chart.width) { + vm.xAlign = "left"; + } else if (vm.x - tooltipWidth / 2 < 0) { + vm.xAlign = "right"; + } + + if (vm.y - tooltipHeight < 0) { + vm.yAlign = "below"; + } + + var tooltipX = vm.x - tooltipWidth / 2, + tooltipY = vm.y - tooltipHeight; + + ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); + + // Custom Tooltips + if (this._custom) { + this._custom(this._view); + } else { + switch (vm.yAlign) { + case "above": + //Draw a caret above the x/y + ctx.beginPath(); + ctx.moveTo(vm.x, vm.y - caretPadding); + ctx.lineTo(vm.x + vm.caretHeight, vm.y - (caretPadding + vm.caretHeight)); + ctx.lineTo(vm.x - vm.caretHeight, vm.y - (caretPadding + vm.caretHeight)); + ctx.closePath(); + ctx.fill(); + break; + case "below": + tooltipY = vm.y + caretPadding + vm.caretHeight; + //Draw a caret below the x/y + ctx.beginPath(); + ctx.moveTo(vm.x, vm.y + caretPadding); + ctx.lineTo(vm.x + vm.caretHeight, vm.y + caretPadding + vm.caretHeight); + ctx.lineTo(vm.x - vm.caretHeight, vm.y + caretPadding + vm.caretHeight); + ctx.closePath(); + ctx.fill(); + break; + } + + switch (vm.xAlign) { + case "left": + tooltipX = vm.x - tooltipWidth + (vm.cornerRadius + vm.caretHeight); + break; + case "right": + tooltipX = vm.x - (vm.cornerRadius + vm.caretHeight); + break; + } + + helpers.drawRoundedRectangle(ctx, tooltipX, tooltipY, tooltipWidth, tooltipRectHeight, vm.cornerRadius); + + ctx.fill(); + + ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString(); + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(vm.text, tooltipX + tooltipWidth / 2, tooltipY + tooltipRectHeight / 2); + + } + break; + case 'label': + + helpers.drawRoundedRectangle(ctx, vm.x, vm.y - vm.height / 2, vm.width, vm.height, vm.cornerRadius); + ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); + ctx.fill(); + ctx.closePath(); + + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = helpers.color(vm.titleTextColor).alpha(vm.opacity).rgbString(); + ctx.font = helpers.fontString(vm.fontSize, vm._titleFontStyle, vm._titleFontFamily); + ctx.fillText(vm.title, vm.x + vm.xPadding, this.getLineHeight(0)); + + ctx.font = helpers.fontString(vm.fontSize, vm._fontStyle, vm._fontFamily); + helpers.each(vm.labels, function(label, index) { + ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString(); + ctx.fillText(label, vm.x + vm.xPadding + vm.fontSize + 3, this.getLineHeight(index + 1)); + + //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) + //ctx.clearRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize/2, vm.fontSize, vm.fontSize); + //Instead we'll make a white filled block to put the legendColour palette over. + + ctx.fillStyle = helpers.color(vm.legendColors[index].stroke).alpha(vm.opacity).rgbString(); + ctx.fillRect(vm.x + vm.xPadding - 1, this.getLineHeight(index + 1) - vm.fontSize / 2 - 1, vm.fontSize + 2, vm.fontSize + 2); + + ctx.fillStyle = helpers.color(vm.legendColors[index].fill).alpha(vm.opacity).rgbString(); + ctx.fillRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize / 2, vm.fontSize, vm.fontSize); + + + }, this); + break; + } + }, + getLineHeight: function(index) { + var baseLineHeight = this._view.y - (this._view.height / 2) + this._view.yPadding, + afterTitleIndex = index - 1; + + //If the index is zero, we're getting the title + if (index === 0) { + return baseLineHeight + this._view.titleFontSize / 2; + } else { + return baseLineHeight + ((this._view.fontSize * 1.5 * afterTitleIndex) + this._view.fontSize / 2) + this._view.titleFontSize * 1.5; + } + + }, + }); + +}).call(this); + +(function() { + + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + Chart.controllers.bar = function(chart, datasetIndex) { + this.initialize.call(this, chart, datasetIndex); + }; + + helpers.extend(Chart.controllers.bar.prototype, { + + initialize: function(chart, datasetIndex) { + this.chart = chart; + this.index = datasetIndex; + this.linkScales(); + this.addElements(); + }, + + linkScales: function() { + if (!this.getDataset().xAxisID) { + this.getDataset().xAxisID = this.chart.options.scales.xAxes[0].id; + } + + if (!this.getDataset().yAxisID) { + this.getDataset().yAxisID = this.chart.options.scales.yAxes[0].id; + } + }, + + getDataset: function() { + return this.chart.data.datasets[this.index]; + }, + + getScaleForId: function(scaleID) { + return this.chart.scales[scaleID]; + }, + + addElements: function() { + this.getDataset().metaData = this.getDataset().metaData || []; + helpers.each(this.getDataset().data, function(value, index) { + this.getDataset().metaData[index] = this.getDataset().metaData[index] || new Chart.elements.Rectangle({ + _chart: this.chart.chart, + _datasetIndex: this.index, + _index: index, + }); + }, this); + }, + + reset: function() { + this.update(true); + }, + + update: function(reset) { + + var xScale = this.getScaleForId(this.getDataset().xAxisID); + var yScale = this.getScaleForId(this.getDataset().yAxisID); + helpers.each(this.getDataset().metaData, function(rectangle, index) { + + var yScalePoint; + + if (yScale.min < 0 && yScale.max < 0) { + // all less than 0. use the top + yScalePoint = yScale.getPixelForValue(yScale.max); + } else if (yScale.min > 0 && yScale.max > 0) { + yScalePoint = yScale.getPixelForValue(yScale.min); + } else { + yScalePoint = yScale.getPixelForValue(0); + } + + helpers.extend(rectangle, { + // Utility + _chart: this.chart.chart, + _xScale: xScale, + _yScale: yScale, + _datasetIndex: this.index, + _index: index, + + + // Desired view properties + _model: { + x: xScale.calculateBarX(this.chart.data.datasets.length, this.index, index), + y: reset ? yScalePoint : yScale.getPixelForValue(this.getDataset().data[index]), + + // Tooltip + label: this.chart.data.labels[index], + datasetLabel: this.getDataset().label, + + // Appearance + base: yScale.calculateBarBase(this.index, index), + width: xScale.calculateBarWidth(this.chart.data.datasets.length), + backgroundColor: rectangle.custom && rectangle.custom.backgroundColor ? rectangle.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.rectangle.backgroundColor), + borderColor: rectangle.custom && rectangle.custom.borderColor ? rectangle.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.rectangle.borderColor), + borderWidth: rectangle.custom && rectangle.custom.borderWidth ? rectangle.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.rectangle.borderWidth), + }, + }); + rectangle.pivot(); + }, this); + }, + + draw: function(ease) { + var easingDecimal = ease || 1; + helpers.each(this.getDataset().metaData, function(rectangle, index) { + rectangle.transition(easingDecimal).draw(); + }, this); + }, + + + + + + + + + // eachLine: function eachLine(callback) { + // helpers.each(this.chart.data.datasets, function(dataset, datasetIndex) { + // if (dataset.metaDataset && dataset.metaDataset instanceof Chart.Line) { + // callback.call(this, dataset, datasetIndex); + // } + // }, this); + // }, + + // addLine: function addLine(dataset, datasetIndex) { + // if (dataset) { + // dataset.metaDataset = new Chart.Line({ + // _chart: this.chart.chart, + // _datasetIndex: datasetIndex, + // _points: dataset.metaData, + // }); + // } + // }, + + // addPoint: function addPoint(dataset, datasetIndex, index) { + // if (dataset) { + // dataset.metaData = dataset.metaData || new Array(this.chart.data.datasets[datasetIndex].data.length); + + // if (index < dataset.metaData.length) { + // dataset.metaData[index] = new Chart.Point({ + // _datasetIndex: datasetIndex, + // _index: index, + // _chart: this.chart.chart, + // _model: { + // x: 0, + // y: 0, + // }, + // }); + // } + // } + // }, + + + + // resetElements: function resetElements() { + // helpers.each(this.chart.data.datasets, function(dataset, datasetIndex) { + // // All elements must be the same type for the given dataset so we are fine to check just the first one + // if (dataset.metaData[0] instanceof Chart.Point) { + // // Have points. Update all of them + // this.resetDatasetPoints(dataset, datasetIndex); + // } else if (dataset.metaData[0] instanceof Chart.Rectangle) { + // // Have rectangles (bars) + // this.resetDatasetRectangles(dataset, datasetIndex); + // } + // }, this); + // }, + + // resetDatasetPoints: function resetDatasetPoints(dataset, datasetIndex) { + // helpers.each(dataset.metaData, function(point, index) { + // var xScale = this.getScaleForId(this.chart.data.datasets[datasetIndex].xAxisID); + // var yScale = this.getScaleForId(this.chart.data.datasets[datasetIndex].yAxisID); + + // var yScalePoint; + + // if (yScale.min < 0 && yScale.max < 0) { + // // all less than 0. use the top + // yScalePoint = yScale.getPixelForValue(yScale.max); + // } else if (yScale.min > 0 && yScale.max > 0) { + // yScalePoint = yScale.getPixelForValue(yScale.min); + // } else { + // yScalePoint = yScale.getPixelForValue(0); + // } + + // helpers.extend(point, { + // // Utility + // _chart: this.chart.chart, //WTF + // _xScale: xScale, + // _yScale: yScale, + // _datasetIndex: datasetIndex, + // _index: index, + + // // Desired view properties + // _model: { + // x: xScale.getPointPixelForValue(this.chart.data.datasets[datasetIndex].data[index], index, datasetIndex), + // y: yScalePoint, + // }, + // }); + + // this.updatePointElementAppearance(point, datasetIndex, index); + // }, this); + + // this.updateBezierControlPoints(dataset); + // }, + + // resetDatasetRectangles: function resetDatasetRectangles(dataset, datasetIndex) { + + // }, + + // resetElementAppearance: function(element, datasetIndex, index) { + // if (element instanceof Chart.Point) { + // this.updatePointElementAppearance(element, datasetIndex, index); + // } else if (element instanceof Chart.Rectangle) { + // this.updateRectangleElementAppearance(element, datasetIndex, index); + // } + // }, + + // updateElements: function updateElements() { + // // Update the lines + // this.updateLines(); + + // helpers.each(this.chart.data.datasets, function(dataset, datasetIndex) { + // // All elements must be the same type for the given dataset so we are fine to check just the first one + // if (dataset.metaData[0] instanceof Chart.Point) { + // // Have points. Update all of them + // this.updatePoints(dataset, datasetIndex); + // } else if (dataset.metaData[0] instanceof Chart.Rectangle) { + // // Have rectangles (bars) + // this.updateRectangles(dataset, datasetIndex); + // } + // }, this); + // }, + + // updateLines: function updateLines() { + // this.eachLine(function(dataset, datasetIndex) { + // var yScale = this.getScaleForId(dataset.yAxisID); + // var scaleBase; + + // if (yScale.min < 0 && yScale.max < 0) { + // scaleBase = yScale.getPixelForValue(yScale.max); + // } else if (yScale.min > 0 && yScale.max > 0) { + // scaleBase = yScale.getPixelForValue(yScale.min); + // } else { + // scaleBase = yScale.getPixelForValue(0); + // } + + // helpers.extend(dataset.metaDataset, { + // // Utility + // _scale: yScale, + // _datasetIndex: datasetIndex, + // // Data + // _children: dataset.metaData, + // // Model + // _model: { + // // Appearance + // tension: dataset.metaDataset.custom && dataset.metaDataset.custom.tension ? dataset.metaDataset.custom.tension : (dataset.tension || this.chart.options.elements.line.tension), + // backgroundColor: dataset.metaDataset.custom && dataset.metaDataset.custom.backgroundColor ? dataset.metaDataset.custom.backgroundColor : (dataset.backgroundColor || this.chart.options.elements.line.backgroundColor), + // borderWidth: dataset.metaDataset.custom && dataset.metaDataset.custom.borderWidth ? dataset.metaDataset.custom.borderWidth : (dataset.borderWidth || this.chart.options.elements.line.borderWidth), + // borderColor: dataset.metaDataset.custom && dataset.metaDataset.custom.borderColor ? dataset.metaDataset.custom.borderColor : (dataset.borderColor || this.chart.options.elements.line.borderColor), + // fill: dataset.metaDataset.custom && dataset.metaDataset.custom.fill ? dataset.metaDataset.custom.fill : (dataset.fill !== undefined ? dataset.fill : this.chart.options.elements.line.fill), + // skipNull: dataset.skipNull !== undefined ? dataset.skipNull : this.chart.options.elements.line.skipNull, + // drawNull: dataset.drawNull !== undefined ? dataset.drawNull : this.chart.options.elements.line.drawNull, + // // Scale + // scaleTop: yScale.top, + // scaleBottom: yScale.bottom, + // scaleZero: scaleBase, + // }, + // }); + + // dataset.metaDataset.pivot(); + // }); + // }, + + // updatePoints: function updatePoints(dataset, datasetIndex) { + // helpers.each(dataset.metaData, function(point, index) { + // var xScale = this.getScaleForId(this.chart.data.datasets[datasetIndex].xAxisID); + // var yScale = this.getScaleForId(this.chart.data.datasets[datasetIndex].yAxisID); + + // helpers.extend(point, { + // // Utility + // _chart: this.chart.chart, + // _xScale: xScale, + // _yScale: yScale, + // _datasetIndex: datasetIndex, + // _index: index, + + // // Desired view properties + // _model: { + // x: xScale.getPointPixelForValue(this.chart.data.datasets[datasetIndex].data[index], index, datasetIndex), + // y: yScale.getPointPixelForValue(this.chart.data.datasets[datasetIndex].data[index], index, datasetIndex), + // }, + // }); + + // this.updatePointElementAppearance(point, datasetIndex, index); + // }, this); + + // this.updateBezierControlPoints(dataset); + // }, + + // updatePointElementAppearance: function updatePointElementAppearance(point, datasetIndex, index) { + // helpers.extend(point._model, { + // // Appearance + // tension: point.custom && point.custom.tension ? point.custom.tension : this.chart.options.elements.line.tension, + // radius: point.custom && point.custom.radius ? point.custom.radius : helpers.getValueAtIndexOrDefault(this.chart.data.datasets[datasetIndex].radius, index, this.chart.options.elements.point.radius), + // backgroundColor: point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.chart.data.datasets[datasetIndex].pointBackgroundColor, index, this.chart.options.elements.point.backgroundColor), + // borderColor: point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.chart.data.datasets[datasetIndex].pointBorderColor, index, this.chart.options.elements.point.borderColor), + // borderWidth: point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.chart.data.datasets[datasetIndex].pointBorderWidth, index, this.chart.options.elements.point.borderWidth), + // skip: this.chart.data.datasets[datasetIndex].data[index] === null, + + // // Tooltip + // hitRadius: point.custom && point.custom.hitRadius ? point.custom.hitRadius : helpers.getValueAtIndexOrDefault(this.chart.data.datasets[datasetIndex].hitRadius, index, this.chart.options.elements.point.hitRadius), + // }); + // }, + + + // setElementHoverStyle: function setElementHoverStyle(element) { + // if (element instanceof Chart.Point) { + // this.setPointHoverStyle(element); + // } else if (element instanceof Chart.Rectangle) { + // this.setRectangleHoverStyle(element); + // } + // }, + + // setPointHoverStyle: function setPointHoverStyle(point) { + // var dataset = this.chart.data.datasets[point._datasetIndex]; + // var index = point._index; + + // point._model.radius = point.custom && point.custom.radius ? point.custom.radius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.chart.options.elements.point.hoverRadius); + // point._model.backgroundColor = point.custom && point.custom.hoverBackgroundColor ? point.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(point._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); + // point._model.borderColor = point.custom && point.custom.hoverBorderColor ? point.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(point._model.borderColor).saturate(0.5).darken(0.1).rgbString()); + // point._model.borderWidth = point.custom && point.custom.hoverBorderWidth ? point.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, point._model.borderWidth); + // }, + + // setHoverStyle: function(rectangle) { + // var dataset = this.chart.data.datasets[rectangle._datasetIndex]; + // var index = rectangle._index; + + // rectangle._model.backgroundColor = rectangle.custom && rectangle.custom.hoverBackgroundColor ? rectangle.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(rectangle._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); + // rectangle._model.borderColor = rectangle.custom && rectangle.custom.hoverBorderColor ? rectangle.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(rectangle._model.borderColor).saturate(0.5).darken(0.1).rgbString()); + // rectangle._model.borderWidth = rectangle.custom && rectangle.custom.hoverBorderWidth ? rectangle.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangle._model.borderWidth); + // }, + + // updateBezierControlPoints: function updateBezierControlPoints(dataset) { + // // Update control points for the bezier curve + // helpers.each(dataset.metaData, function(point, index) { + // var controlPoints = helpers.splineCurve( + // this.previousPoint(dataset.metaData, index)._model, + // point._model, + // this.nextPoint(dataset.metaData, index)._model, + // point._model.tension + // ); + + // point._model.controlPointPreviousX = controlPoints.previous.x; + // point._model.controlPointNextX = controlPoints.next.x; + + // // Prevent the bezier going outside of the bounds of the graph + + // // Cap puter bezier handles to the upper/lower scale bounds + // if (controlPoints.next.y > this.chart.chartArea.bottom) { + // point._model.controlPointNextY = this.chart.chartArea.bottom; + // } else if (controlPoints.next.y < this.chart.chartArea.top) { + // point._model.controlPointNextY = this.chart.chartArea.top; + // } else { + // point._model.controlPointNextY = controlPoints.next.y; + // } + + // // Cap inner bezier handles to the upper/lower scale bounds + // if (controlPoints.previous.y > this.chart.chartArea.bottom) { + // point._model.controlPointPreviousY = this.chart.chartArea.bottom; + // } else if (controlPoints.previous.y < this.chart.chartArea.top) { + // point._model.controlPointPreviousY = this.chart.chartArea.top; + // } else { + // point._model.controlPointPreviousY = controlPoints.previous.y; + // } + + // // Now pivot the point for animation + // point.pivot(); + // }, this); + // }, + + getElementsAtEvent: function(e) { + var elementsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset) { + elementsArray.push(dataset.metaData[elementIndex]); + }, + elementIndex; + + for (var datasetIndex = 0; datasetIndex < this.chart.data.datasets.length; datasetIndex++) { + for (elementIndex = 0; elementIndex < this.chart.data.datasets[datasetIndex].metaData.length; elementIndex++) { + if (this.chart.data.datasets[datasetIndex].metaData[elementIndex].inGroupRange(eventPosition.x, eventPosition.y)) { + helpers.each(this.chart.data.datasets, datasetIterator); + } + } + } + + return elementsArray.length ? elementsArray : []; + }, + + // // Get the single element that was clicked on + // // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was drawn + getElementAtEvent: function(e) { + var element = []; + var eventPosition = helpers.getRelativePosition(e); + + for (var datasetIndex = 0; datasetIndex < this.chart.data.datasets.length; ++datasetIndex) { + for (var elementIndex = 0; elementIndex < this.chart.data.datasets[datasetIndex].metaData.length; ++elementIndex) { + if (this.chart.data.datasets[datasetIndex].metaData[elementIndex].inRange(eventPosition.x, eventPosition.y)) { + element.push(this.chart.data.datasets[datasetIndex].metaData[elementIndex]); + return element; + } + } + } + + return []; + }, + }); }).call(this); (function() { - "use strict"; - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; + "use strict"; - var LinearScale = Chart.Element.extend({ - calculateRange: helpers.noop, // overridden in the chart. Will set min and max as properties of the scale for later use - isHorizontal: function() { - return this.options.position == "top" || this.options.position == "bottom"; - }, - generateTicks: function(width, height) { - // We need to decide how many ticks we are going to have. Each tick draws a grid line. - // There are two possibilities. The first is that the user has manually overridden the scale - // calculations in which case the job is easy. The other case is that we have to do it ourselves - // - // We assume at this point that the scale object has been updated with the following values - // by the chart. - // min: this is the minimum value of the scale - // max: this is the maximum value of the scale - // options: contains the options for the scale. This is referenced from the user settings - // rather than being cloned. This ensures that updates always propogate to a redraw + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; - // Reset the ticks array. Later on, we will draw a grid line at these positions - // The array simply contains the numerical value of the spots where ticks will be - this.ticks = []; + Chart.controllers.line = function(chart, datasetIndex) { + this.initialize.call(this, chart, datasetIndex); + }; - if (this.options.override) { - // The user has specified the manual override. We use <= instead of < so that - // we get the final line - for (var i = 0; i <= this.options.override.steps; ++i) { - var value = this.options.override.start + (i * this.options.override.stepWidth); - ticks.push(value); - } - } else { - // Figure out what the max number of ticks we can support it is based on the size of - // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 - // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on - // the graph + helpers.extend(Chart.controllers.line.prototype, { - var maxTicks; + initialize: function(chart, datasetIndex) { + this.chart = chart; + this.index = datasetIndex; + this.linkScales(); + this.addElements(); + }, - if (this.isHorizontal()) { - maxTicks = Math.min(11, Math.ceil(width / 50)); - } else { - // The factor of 2 used to scale the font size has been experimentally determined. - maxTicks = Math.min(11, Math.ceil(height / (2 * this.options.labels.fontSize))); - } + linkScales: function() { + if (!this.getDataset().xAxisID) { + this.getDataset().xAxisID = this.chart.options.scales.xAxes[0].id; + } - // Make sure we always have at least 2 ticks - maxTicks = Math.max(2, maxTicks); + if (!this.getDataset().yAxisID) { + this.getDataset().yAxisID = this.chart.options.scales.yAxes[0].id; + } + }, - // To get a "nice" value for the tick spacing, we will use the appropriately named - // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks - // for details. + getDataset: function() { + return this.chart.data.datasets[this.index]; + }, - // If we are forcing it to begin at 0, but 0 will already be rendered on the chart, - // do nothing since that would make the chart weird. If the user really wants a weird chart - // axis, they can manually override it - if (this.options.beginAtZero) { - var minSign = helpers.sign(this.min); - var maxSign = helpers.sign(this.max); + getScaleForId: function(scaleID) { + return this.chart.scales[scaleID]; + }, - if (minSign < 0 && maxSign < 0) { - // move the top up to 0 - this.max = 0; - } else if (minSign > 0 && maxSign > 0) { - // move the botttom down to 0 - this.min = 0; - } - } + addElements: function() { - var niceRange = helpers.niceNum(this.max - this.min, false); - var spacing = helpers.niceNum(niceRange / (maxTicks - 1), true); - var niceMin = Math.floor(this.min / spacing) * spacing; - var niceMax = Math.ceil(this.max / spacing) * spacing; + this.getDataset().metaData = this.getDataset().metaData || []; - // Put the values into the ticks array - for (var j = niceMin; j <= niceMax; j += spacing) { - this.ticks.push(j); - } - } + this.getDataset().metaDataset = this.getDataset().metaDataset || new Chart.elements.Line({ + _chart: this.chart.chart, + _datasetIndex: this.index, + _points: this.getDataset().metaData, + }); - if (this.options.position == "left" || this.options.position == "right") { - // We are in a vertical orientation. The top value is the highest. So reverse the array - this.ticks.reverse(); - } + helpers.each(this.getDataset().data, function(value, index) { + this.getDataset().metaData[index] = this.getDataset().metaData[index] || new Chart.elements.Point({ + _chart: this.chart.chart, + _datasetIndex: this.index, + _index: index, + }); + }, this); + }, - // At this point, we need to update our max and min given the tick values since we have expanded the - // range of the scale - this.max = helpers.max(this.ticks); - this.min = helpers.min(this.ticks); - }, - buildLabels: function() { - // We assume that this has been run after ticks have been generated. We try to figure out - // a label for each tick. - this.labels = []; + reset: function() { + this.update(true); + }, - helpers.each(this.ticks, function(tick, index, ticks) { - var label; + update: function(reset) { - if (this.options.labels.userCallback) { - // If the user provided a callback for label generation, use that as first priority - label = this.options.lables.userCallback(tick, index, ticks); - } else if (this.options.labels.template) { - // else fall back to the template string - label = helpers.template(this.options.labels.template, { - value: tick - }); - } + var line = this.getDataset().metaDataset; + var points = this.getDataset().metaData; - this.labels.push(label ? label : ""); // empty string will not render so we're good - }, this); - }, - getPixelForValue: function(value) { - // This must be called after fit has been run so that - // this.left, this.top, this.right, and this.bottom have been defined - var pixel; - var range = this.max - this.min; + var yScale = this.getScaleForId(this.getDataset().yAxisID); + var xScale = this.getScaleForId(this.getDataset().xAxisID); + var scaleBase; - if (this.isHorizontal()) { - pixel = this.left + (this.width / range * (value - this.min)); - } else { - // Bottom - top since pixels increase downard on a screen - pixel = this.bottom - (this.height / range * (value - this.min)); - } - - return pixel; - }, - // Fit this axis to the given size - // @param {number} maxWidth : the max width the axis can be - // @param {number} maxHeight: the max height the axis can be - // @return {object} minSize : the minimum size needed to draw the axis - fit: function(maxWidth, maxHeight) { - this.calculateRange(); - this.generateTicks(maxWidth, maxHeight); - this.buildLabels(); - - var minSize = { - width: 0, - height: 0, - }; - - // In a horizontal axis, we need some room for the scale to be drawn - // - // ----------------------------------------------------- - // | | | | | - // - // In a vertical axis, we need some room for the scale to be drawn. - // The actual grid lines will be drawn on the chart area, however, we need to show - // ticks where the axis actually is. - // We will allocate 25px for this width - // | - // -| - // | - // | - // -| - // | - // | - // -| + if (yScale.min < 0 && yScale.max < 0) { + scaleBase = yScale.getPixelForValue(yScale.max); + } else if (yScale.min > 0 && yScale.max > 0) { + scaleBase = yScale.getPixelForValue(yScale.min); + } else { + scaleBase = yScale.getPixelForValue(0); + } - // Width - if (this.isHorizontal()) { - minSize.width = maxWidth; // fill all the width - } else { - minSize.width = this.options.gridLines.show && this.options.display ? 10 : 0; - } + // Update Line + helpers.extend(line, { + // Utility + _scale: yScale, + _datasetIndex: this.index, + // Data + _children: points, + // Model + _model: { + // Appearance + tension: line.custom && line.custom.tension ? line.custom.tension : (this.getDataset().tension || this.chart.options.elements.line.tension), + backgroundColor: line.custom && line.custom.backgroundColor ? line.custom.backgroundColor : (this.getDataset().backgroundColor || this.chart.options.elements.line.backgroundColor), + borderWidth: line.custom && line.custom.borderWidth ? line.custom.borderWidth : (this.getDataset().borderWidth || this.chart.options.elements.line.borderWidth), + borderColor: line.custom && line.custom.borderColor ? line.custom.borderColor : (this.getDataset().borderColor || this.chart.options.elements.line.borderColor), + fill: line.custom && line.custom.fill ? line.custom.fill : (this.getDataset().fill !== undefined ? this.getDataset().fill : this.chart.options.elements.line.fill), + skipNull: this.getDataset().skipNull !== undefined ? this.getDataset().skipNull : this.chart.options.elements.line.skipNull, + drawNull: this.getDataset().drawNull !== undefined ? this.getDataset().drawNull : this.chart.options.elements.line.drawNull, + // Scale + scaleTop: yScale.top, + scaleBottom: yScale.bottom, + scaleZero: scaleBase, + }, + }); + line.pivot(); - // height - if (this.isHorizontal()) { - minSize.height = this.options.gridLines.show && this.options.display ? 10 : 0; - } else { - minSize.height = maxHeight; // fill all the height - } + // Update Points + helpers.each(points, function(point, index) { + helpers.extend(point, { + // Utility + _chart: this.chart.chart, + _xScale: xScale, + _yScale: yScale, + _datasetIndex: this.index, + _index: index, + + // Desired view properties + _model: { + x: xScale.getPointPixelForValue(this.getDataset().data[index], index, this.index), + y: yScale.getPointPixelForValue(this.getDataset().data[index], index, this.index), + // Appearance + tension: point.custom && point.custom.tension ? point.custom.tension : this.chart.options.elements.line.tension, + radius: point.custom && point.custom.radius ? point.custom.radius : helpers.getValueAtIndexOrDefault(this.getDataset().radius, index, this.chart.options.elements.point.radius), + backgroundColor: point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().pointBackgroundColor, index, this.chart.options.elements.point.backgroundColor), + borderColor: point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().pointBorderColor, index, this.chart.options.elements.point.borderColor), + borderWidth: point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().pointBorderWidth, index, this.chart.options.elements.point.borderWidth), + skip: this.getDataset().data[index] === null, + + // Tooltip + hitRadius: point.custom && point.custom.hitRadius ? point.custom.hitRadius : helpers.getValueAtIndexOrDefault(this.getDataset().hitRadius, index, this.chart.options.elements.point.hitRadius), + }, + + }); + }, this); + + // Update bezier control points + helpers.each(this.getDataset().metaData, function(point, index) { + var controlPoints = helpers.splineCurve( + helpers.previousItem(this.getDataset().metaData, index)._model, + point._model, + helpers.nextItem(this.getDataset().metaData, index)._model, + point._model.tension + ); + + point._model.controlPointPreviousX = controlPoints.previous.x; + point._model.controlPointNextX = controlPoints.next.x; + + // Prevent the bezier going outside of the bounds of the graph + + // Cap puter bezier handles to the upper/lower scale bounds + if (controlPoints.next.y > this.chart.chartArea.bottom) { + point._model.controlPointNextY = this.chart.chartArea.bottom; + } else if (controlPoints.next.y < this.chart.chartArea.top) { + point._model.controlPointNextY = this.chart.chartArea.top; + } else { + point._model.controlPointNextY = controlPoints.next.y; + } + + // Cap inner bezier handles to the upper/lower scale bounds + if (controlPoints.previous.y > this.chart.chartArea.bottom) { + point._model.controlPointPreviousY = this.chart.chartArea.bottom; + } else if (controlPoints.previous.y < this.chart.chartArea.top) { + point._model.controlPointPreviousY = this.chart.chartArea.top; + } else { + point._model.controlPointPreviousY = controlPoints.previous.y; + } + + // Now pivot the point for animation + point.pivot(); + }, this); + + }, + + draw: function(ease) { + var easingDecimal = ease || 1; + + // Transition Point Locations + helpers.each(this.getDataset().metaData, function(point, index) { + point.transition(easingDecimal); + }, this); + + // Transition and Draw the line + this.getDataset().metaDataset.transition(easingDecimal).draw(); + + // Draw the points + helpers.each(this.getDataset().metaData, function(point) { + point.draw(); + }); + }, - if (this.options.labels.show && this.options.display) { - // Don't bother fitting the labels if we are not showing them - var labelFont = helpers.fontString(this.options.labels.fontSize, - this.options.labels.fontStyle, this.options.labels.fontFamily); - if (this.isHorizontal()) { - // A horizontal axis is more constrained by the height. - var maxLabelHeight = maxHeight - minSize.height; - var labelHeight = 1.5 * this.options.labels.fontSize; - minSize.height = Math.min(maxHeight, minSize.height + labelHeight); - } else { - // A vertical axis is more constrained by the width. Labels are the dominant factor - // here, so get that length first - var maxLabelWidth = maxWidth - minSize.width; - var largestTextWidth = helpers.longestText(this.ctx, labelFont, this.labels); - if (largestTextWidth < maxLabelWidth) { - // We don't need all the room - minSize.width += largestTextWidth; - } else { - // Expand to max size - minSize.width = maxWidth; - } - } - } - this.width = minSize.width; - this.height = minSize.height; - return minSize; - }, - // Actualy draw the scale on the canvas - // @param {rectangle} chartArea : the area of the chart to draw full grid lines on - draw: function(chartArea) { - if (this.options.display) { - var setContextLineSettings; - var hasZero; - // Make sure we draw text in the correct color - this.ctx.fillStyle = this.options.labels.fontColor; + // eachLine: function eachLine(callback) { + // helpers.each(this.chart.data.datasets, function(dataset, datasetIndex) { + // if (dataset.metaDataset && dataset.metaDataset instanceof Chart.Line) { + // callback.call(this, dataset, datasetIndex); + // } + // }, this); + // }, - if (this.isHorizontal()) { - if (this.options.gridLines.show) { - // Draw the horizontal line - setContextLineSettings = true; - hasZero = helpers.findNextWhere(this.ticks, function(tick) { - return tick === 0; - }) !== undefined; - var yTickStart = this.options.position == "bottom" ? this.top : this.bottom - 5; - var yTickEnd = this.options.position == "bottom" ? this.top + 5 : this.bottom; + // addPoint: function addPoint(dataset, datasetIndex, index) { + // if (dataset) { + // dataset.metaData = dataset.metaData || new Array(this.chart.data.datasets[datasetIndex].data.length); - helpers.each(this.ticks, function(tick, index) { - // Grid lines are vertical - var xValue = this.getPixelForValue(tick); + // if (index < dataset.metaData.length) { + // dataset.metaData[index] = new Chart.Point({ + // _datasetIndex: datasetIndex, + // _index: index, + // _chart: this.chart.chart, + // _model: { + // x: 0, + // y: 0, + // }, + // }); + // } + // } + // }, - if (tick === 0 || (!hasZero && index === 0)) { - // Draw the 0 point specially or the left if there is no 0 - this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; - this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; - setContextLineSettings = true; // reset next time - } else if (setContextLineSettings) { - this.ctx.lineWidth = this.options.gridLines.lineWidth; - this.ctx.strokeStyle = this.options.gridLines.color; - setContextLineSettings = false; - } - xValue += helpers.aliasPixel(this.ctx.lineWidth); - // Draw the label area - this.ctx.beginPath(); + // resetElements: function resetElements() { + // helpers.each(this.chart.data.datasets, function(dataset, datasetIndex) { + // // All elements must be the same type for the given dataset so we are fine to check just the first one + // if (dataset.metaData[0] instanceof Chart.Point) { + // // Have points. Update all of them + // this.resetDatasetPoints(dataset, datasetIndex); + // } else if (dataset.metaData[0] instanceof Chart.Rectangle) { + // // Have rectangles (lines) + // this.resetDatasetRectangles(dataset, datasetIndex); + // } + // }, this); + // }, - if (this.options.gridLines.drawTicks) { - this.ctx.moveTo(xValue, yTickStart); - this.ctx.lineTo(xValue, yTickEnd); - } + // resetDatasetPoints: function resetDatasetPoints(dataset, datasetIndex) { + // helpers.each(dataset.metaData, function(point, index) { + // var xScale = this.getScaleForId(this.chart.data.datasets[datasetIndex].xAxisID); + // var yScale = this.getScaleForId(this.chart.data.datasets[datasetIndex].yAxisID); - // Draw the chart area - if (this.options.gridLines.drawOnChartArea) { - this.ctx.moveTo(xValue, chartArea.top); - this.ctx.lineTo(xValue, chartArea.bottom); - } + // var yScalePoint; - // Need to stroke in the loop because we are potentially changing line widths & colours - this.ctx.stroke(); - }, this); - } + // if (yScale.min < 0 && yScale.max < 0) { + // // all less than 0. use the top + // yScalePoint = yScale.getPixelForValue(yScale.max); + // } else if (yScale.min > 0 && yScale.max > 0) { + // yScalePoint = yScale.getPixelForValue(yScale.min); + // } else { + // yScalePoint = yScale.getPixelForValue(0); + // } - if (this.options.labels.show) { - // Draw the labels + // helpers.extend(point, { + // // Utility + // _chart: this.chart.chart, //WTF + // _xScale: xScale, + // _yScale: yScale, + // _datasetIndex: datasetIndex, + // _index: index, - var labelStartY; + // // Desired view properties + // _model: { + // x: xScale.getPointPixelForValue(this.chart.data.datasets[datasetIndex].data[index], index, datasetIndex), + // y: yScalePoint, + // }, + // }); - if (this.options.position == "top") { - labelStartY = this.bottom - 10; - this.ctx.textBaseline = "bottom"; - } else { - // bottom side - labelStartY = this.top + 10; - this.ctx.textBaseline = "top"; - } + // this.updatePointElementAppearance(point, datasetIndex, index); + // }, this); - this.ctx.textAlign = "center"; - this.ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + // this.updateBezierControlPoints(dataset); + // }, - helpers.each(this.labels, function(label, index) { - var xValue = this.getPixelForValue(this.ticks[index]); - this.ctx.fillText(label, xValue, labelStartY); - }, this); - } - } else { - // Vertical - if (this.options.gridLines.show) { + // resetDatasetRectangles: function resetDatasetRectangles(dataset, datasetIndex) { - // Draw the vertical line - setContextLineSettings = true; - hasZero = helpers.findNextWhere(this.ticks, function(tick) { - return tick === 0; - }) !== undefined; - var xTickStart = this.options.position == "right" ? this.left : this.right - 5; - var xTickEnd = this.options.position == "right" ? this.left + 5 : this.right; + // }, - helpers.each(this.ticks, function(tick, index) { - // Grid lines are horizontal - var yValue = this.getPixelForValue(tick); + // resetElementAppearance: function(element, datasetIndex, index) { + // if (element instanceof Chart.Point) { + // this.updatePointElementAppearance(element, datasetIndex, index); + // } else if (element instanceof Chart.Rectangle) { + // this.updateRectangleElementAppearance(element, datasetIndex, index); + // } + // }, - if (tick === 0 || (!hasZero && index === 0)) { - // Draw the 0 point specially or the bottom if there is no 0 - this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; - this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; - setContextLineSettings = true; // reset next time - } else if (setContextLineSettings) { - this.ctx.lineWidth = this.options.gridLines.lineWidth; - this.ctx.strokeStyle = this.options.gridLines.color; - setContextLineSettings = false; // use boolean to indicate that we only want to do this once - } + // updateElements: function updateElements() { + // // Update the lines + // this.updateLines(); - yValue += helpers.aliasPixel(this.ctx.lineWidth); + // helpers.each(this.chart.data.datasets, function(dataset, datasetIndex) { + // // All elements must be the same type for the given dataset so we are fine to check just the first one + // if (dataset.metaData[0] instanceof Chart.Point) { + // // Have points. Update all of them + // this.updatePoints(dataset, datasetIndex); + // } else if (dataset.metaData[0] instanceof Chart.Rectangle) { + // // Have rectangles (lines) + // this.updateRectangles(dataset, datasetIndex); + // } + // }, this); + // }, - // Draw the label area - this.ctx.beginPath(); - if (this.options.gridLines.drawTicks) { - this.ctx.moveTo(xTickStart, yValue); - this.ctx.lineTo(xTickEnd, yValue); - } + // setElementHoverStyle: function setElementHoverStyle(element) { + // if (element instanceof Chart.Point) { + // this.setPointHoverStyle(element); + // } else if (element instanceof Chart.Rectangle) { + // this.setRectangleHoverStyle(element); + // } + // }, - // Draw the chart area - if (this.options.gridLines.drawOnChartArea) { - this.ctx.moveTo(chartArea.left, yValue); - this.ctx.lineTo(chartArea.right, yValue); - } + // setPointHoverStyle: function setPointHoverStyle(point) { + // var dataset = this.chart.data.datasets[point._datasetIndex]; + // var index = point._index; - this.ctx.stroke(); - }, this); - } + // point._model.radius = point.custom && point.custom.radius ? point.custom.radius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.chart.options.elements.point.hoverRadius); + // point._model.backgroundColor = point.custom && point.custom.hoverBackgroundColor ? point.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(point._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); + // point._model.borderColor = point.custom && point.custom.hoverBorderColor ? point.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(point._model.borderColor).saturate(0.5).darken(0.1).rgbString()); + // point._model.borderWidth = point.custom && point.custom.hoverBorderWidth ? point.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, point._model.borderWidth); + // }, - if (this.options.labels.show) { - // Draw the labels + // setHoverStyle: function(rectangle) { + // var dataset = this.chart.data.datasets[rectangle._datasetIndex]; + // var index = rectangle._index; - var labelStartX; + // rectangle._model.backgroundColor = rectangle.custom && rectangle.custom.hoverBackgroundColor ? rectangle.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(rectangle._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); + // rectangle._model.borderColor = rectangle.custom && rectangle.custom.hoverBorderColor ? rectangle.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(rectangle._model.borderColor).saturate(0.5).darken(0.1).rgbString()); + // rectangle._model.borderWidth = rectangle.custom && rectangle.custom.hoverBorderWidth ? rectangle.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangle._model.borderWidth); + // }, - if (this.options.position == "left") { - labelStartX = this.right - 10; - this.ctx.textAlign = "right"; - } else { - // right side - labelStartX = this.left + 5; - this.ctx.textAlign = "left"; - } + getElementsAtEvent: function(e) { + var elementsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset) { + elementsArray.push(dataset.metaData[elementIndex]); + }, + elementIndex; - this.ctx.textBaseline = "middle"; - this.ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + for (var datasetIndex = 0; datasetIndex < this.chart.data.datasets.length; datasetIndex++) { + for (elementIndex = 0; elementIndex < this.chart.data.datasets[datasetIndex].metaData.length; elementIndex++) { + if (this.chart.data.datasets[datasetIndex].metaData[elementIndex].inGroupRange(eventPosition.x, eventPosition.y)) { + helpers.each(this.chart.data.datasets, datasetIterator); + } + } + } + + return elementsArray.length ? elementsArray : []; + }, + + // // Get the single element that was clicked on + // // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was drawn + getElementAtEvent: function(e) { + var element = []; + var eventPosition = helpers.getRelativePosition(e); + + for (var datasetIndex = 0; datasetIndex < this.chart.data.datasets.length; ++datasetIndex) { + for (var elementIndex = 0; elementIndex < this.chart.data.datasets[datasetIndex].metaData.length; ++elementIndex) { + if (this.chart.data.datasets[datasetIndex].metaData[elementIndex].inRange(eventPosition.x, eventPosition.y)) { + element.push(this.chart.data.datasets[datasetIndex].metaData[elementIndex]); + return element; + } + } + } + + return []; + }, + }); - helpers.each(this.labels, function(label, index) { - var yValue = this.getPixelForValue(this.ticks[index]); - this.ctx.fillText(label, labelStartX, yValue); - }, this); - } - } - } - } - }); - Chart.scales.registerScaleType("linear", LinearScale); }).call(this); (function() { - "use strict"; + "use strict"; - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; - var LinearRadialScale = Chart.Element.extend({ - initialize: function() { - this.size = helpers.min([this.height, this.width]); - this.drawingArea = (this.options.display) ? (this.size / 2) - (this.options.labels.fontSize / 2 + this.options.labels.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.options.lineArc) { - this.setScaleSize(); - } else { - this.drawingArea = (this.options.display) ? (this.size / 2) - (this.fontSize / 2 + this.backdropPaddingY) : (this.size / 2); - } + // Default config for a category scale + var defaultConfig = { + display: true, + position: "bottom", + id: "x-axis-1", // need an ID so datasets can reference the scale - this.buildYLabels(); - }, - calculateRange: helpers.noop, // overridden in chart - generateTicks: function() { - // We need to decide how many ticks we are going to have. Each tick draws a grid line. - // There are two possibilities. The first is that the user has manually overridden the scale - // calculations in which case the job is easy. The other case is that we have to do it ourselves - // - // We assume at this point that the scale object has been updated with the following values - // by the chart. - // min: this is the minimum value of the scale - // max: this is the maximum value of the scale - // options: contains the options for the scale. This is referenced from the user settings - // rather than being cloned. This ensures that updates always propogate to a redraw + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.1)", + lineWidth: 1, + drawOnChartArea: true, + drawTicks: true, + zeroLineWidth: 1, + zeroLineColor: "rgba(0,0,0,0.25)", + offsetGridLines: false, + }, - // Reset the ticks array. Later on, we will draw a grid line at these positions - // The array simply contains the numerical value of the spots where ticks will be - this.ticks = []; + // label settings + labels: { + show: true, + template: "<%=value%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue", + }, + }; - if (this.options.override) { - // The user has specified the manual override. We use <= instead of < so that - // we get the final line - for (var i = 0; i <= this.options.override.steps; ++i) { - var value = this.options.override.start + (i * this.options.override.stepWidth); - ticks.push(value); - } - } else { - // Figure out what the max number of ticks we can support it is based on the size of - // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 - // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on - // the graph + var DatasetScale = Chart.Element.extend({ + isHorizontal: function() { + return this.options.position == "top" || this.options.position == "bottom"; + }, + getPixelForValue: function(value, index, datasetIndex, includeOffset) { + // This must be called after fit has been run so that + // this.left, this.top, this.right, and this.bottom have been defined + if (this.isHorizontal()) { + var isRotated = (this.labelRotation > 0); + var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + var valueWidth = innerWidth / Math.max((this.data.labels.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1); + var valueOffset = (valueWidth * index) + this.paddingLeft; - var maxTicks = Math.min(11, Math.ceil(this.drawingArea / (2 * this.options.labels.fontSize))); + if (this.options.gridLines.offsetGridLines && includeOffset) { + valueOffset += (valueWidth / 2); + } - // Make sure we always have at least 2 ticks - maxTicks = Math.max(2, maxTicks); + return this.left + Math.round(valueOffset); + } else { + return this.top + (index * (this.height / this.data.labels.length)); + } + }, + getPointPixelForValue: function(value, index, datasetIndex) { + return this.getPixelForValue(value, index, datasetIndex, true); + }, - // To get a "nice" value for the tick spacing, we will use the appropriately named - // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks - // for details. + // Functions needed for bar charts + calculateBaseWidth: function() { + return (this.getPixelForValue(null, 1, 0, true) - this.getPixelForValue(null, 0, 0, true)) - (2 * this.options.categorySpacing); + }, + 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) * this.options.spacing); - // If we are forcing it to begin at 0, but 0 will already be rendered on the chart, - // do nothing since that would make the chart weird. If the user really wants a weird chart - // axis, they can manually override it - if (this.options.beginAtZero) { - var minSign = helpers.sign(this.min); - var maxSign = helpers.sign(this.max); + if (this.options.stacked) { + return baseWidth; + } + return (baseWidth / datasetCount); + }, + calculateBarX: function(datasetCount, datasetIndex, elementIndex) { + var xWidth = this.calculateBaseWidth(), + xAbsolute = this.getPixelForValue(null, elementIndex, datasetIndex, true) - (xWidth / 2), + barWidth = this.calculateBarWidth(datasetCount); - if (minSign < 0 && maxSign < 0) { - // move the top up to 0 - this.max = 0; - } else if (minSign > 0 && maxSign > 0) { - // move the botttom down to 0 - this.min = 0; - } - } + if (this.options.stacked) { + return xAbsolute + barWidth / 2; + } - var niceRange = helpers.niceNum(this.max - this.min, false); - var spacing = helpers.niceNum(niceRange / (maxTicks - 1), true); - var niceMin = Math.floor(this.min / spacing) * spacing; - var niceMax = Math.ceil(this.max / spacing) * spacing; + return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * this.options.spacing) + barWidth / 2; + }, - // Put the values into the ticks array - for (var j = niceMin; j <= niceMax; j += spacing) { - this.ticks.push(j); - } - } + calculateLabelRotation: function(maxHeight, margins) { + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + this.ctx.font = labelFont; - if (this.options.position == "left" || this.options.position == "right") { - // We are in a vertical orientation. The top value is the highest. So reverse the array - this.ticks.reverse(); - } + var firstWidth = this.ctx.measureText(this.data.labels[0]).width; + var lastWidth = this.ctx.measureText(this.data.labels[this.data.labels.length - 1]).width; + var firstRotated; + var lastRotated; - // At this point, we need to update our max and min given the tick values since we have expanded the - // range of the scale - this.max = helpers.max(this.ticks); - this.min = helpers.min(this.ticks); - }, - buildYLabels: function() { - this.yLabels = []; + this.paddingRight = lastWidth / 2 + 3; + this.paddingLeft = firstWidth / 2 + 3; - helpers.each(this.ticks, function(tick, index, ticks) { - var label; + this.labelRotation = 0; - if (this.options.labels.userCallback) { - // If the user provided a callback for label generation, use that as first priority - label = this.options.labels.userCallback(tick, index, ticks); - } else if (this.options.labels.template) { - // else fall back to the template string - label = helpers.template(this.options.labels.template, { - value: tick - }); - } + if (this.options.display) { + var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.data.labels); + var cosRotation; + var sinRotation; + var firstRotatedWidth; - this.yLabels.push(label ? label : ""); - }, this); - }, - 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 - */ + this.labelWidth = originalLabelWidth; + + //Allow 3 pixels x2 padding either side for label readability + // only the index matters for a dataset scale, but we want a consistent interface between scales + var gridWidth = Math.floor(this.getPixelForValue(0, 1) - this.getPixelForValue(0, 0)) - 6; + + //Max label rotate should be 90 - also act as a loop counter + while ((this.labelWidth > gridWidth && this.labelRotation === 0) || (this.labelWidth > gridWidth && this.labelRotation <= 90 && this.labelRotation > 0)) { + cosRotation = Math.cos(helpers.toRadians(this.labelRotation)); + sinRotation = Math.sin(helpers.toRadians(this.labelRotation)); + + firstRotated = cosRotation * firstWidth; + lastRotated = cosRotation * lastWidth; + + // We're right aligning the text now. + if (firstRotated + this.options.labels.fontSize / 2 > this.yLabelWidth) { + this.paddingLeft = firstRotated + this.options.labels.fontSize / 2; + } + + this.paddingRight = this.options.labels.fontSize / 2; + + if (sinRotation * originalLabelWidth > maxHeight) { + // go back one step + this.labelRotation--; + break; + } + + this.labelRotation++; + this.labelWidth = cosRotation * originalLabelWidth; + + } + } else { + this.labelWidth = 0; + this.paddingRight = 0; + this.paddingLeft = 0; + } + + if (margins) { + this.paddingLeft -= margins.left; + this.paddingRight -= margins.right; + + this.paddingLeft = Math.max(this.paddingLeft, 0); + this.paddingRight = Math.max(this.paddingRight, 0); + } + + }, + // Fit this axis to the given size + // @param {number} maxWidth : the max width the axis can be + // @param {number} maxHeight: the max height the axis can be + // @return {object} minSize : the minimum size needed to draw the axis + fit: function(maxWidth, maxHeight, margins) { + this.calculateLabelRotation(maxHeight, margins); + + var minSize = { + width: 0, + height: 0, + }; + + var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + var longestLabelWidth = helpers.longestText(this.ctx, labelFont, this.data.labels); + + // Width + if (this.isHorizontal()) { + minSize.width = maxWidth; + this.width = maxWidth; + } else if (this.options.display) { + minSize.width = Math.min(longestLabelWidth + 6, maxWidth); + } + + // Height + if (this.isHorizontal() && this.options.display) { + var labelHeight = (Math.cos(helpers.toRadians(this.labelRotation)) * longestLabelWidth) + 1.5 * this.options.labels.fontSize; + minSize.height = Math.min(labelHeight, maxHeight); + } else if (this.options.display) { + minSize.width = Math.min(longestLabelWidth + 6, maxWidth); + } + + this.width = minSize.width; + this.height = minSize.height; + return minSize; + }, + // Actualy draw the scale on the canvas + // @param {rectangle} chartArea : the area of the chart to draw full grid lines on + draw: function(chartArea) { + if (this.options.display) { + + var setContextLineSettings; + + // Make sure we draw text in the correct color + this.ctx.fillStyle = this.options.labels.fontColor; + + if (this.isHorizontal()) { + setContextLineSettings = true; + var yTickStart = this.options.position == "bottom" ? this.top : this.bottom - 10; + var yTickEnd = this.options.position == "bottom" ? this.top + 10 : this.bottom; + var isRotated = this.labelRotation !== 0; + + helpers.each(this.data.labels, function(label, index) { + var xLineValue = this.getPixelForValue(label, index, null, false); // xvalues for grid lines + var xLabelValue = this.getPixelForValue(label, index, null, true); // x values for labels (need to consider offsetLabel option) + + if (this.options.gridLines.show) { + if (index === 0) { + // Draw the first index specially + this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; + this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; + setContextLineSettings = true; // reset next time + } else if (setContextLineSettings) { + this.ctx.lineWidth = this.options.gridLines.lineWidth; + this.ctx.strokeStyle = this.options.gridLines.color; + setContextLineSettings = false; + } + + xLineValue += helpers.aliasPixel(this.ctx.lineWidth); + + // Draw the label area + this.ctx.beginPath(); + + if (this.options.gridLines.drawTicks) { + this.ctx.moveTo(xLineValue, yTickStart); + this.ctx.lineTo(xLineValue, yTickEnd); + } + + // Draw the chart area + if (this.options.gridLines.drawOnChartArea) { + this.ctx.moveTo(xLineValue, chartArea.top); + this.ctx.lineTo(xLineValue, chartArea.bottom); + } + + // Need to stroke in the loop because we are potentially changing line widths & colours + this.ctx.stroke(); + } + + if (this.options.labels.show) { + this.ctx.save(); + this.ctx.translate(xLabelValue, (isRotated) ? this.top + 12 : this.top + 8); + this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1); + this.ctx.font = this.font; + this.ctx.textAlign = (isRotated) ? "right" : "center"; + this.ctx.textBaseline = (isRotated) ? "middle" : "top"; + this.ctx.fillText(label, 0, 0); + this.ctx.restore(); + } + }, this); + } else { + // Vertical + if (this.options.gridLines.show) {} + + if (this.options.labels.show) { + // Draw the labels + } + } + } + } + }); + Chart.scaleService.registerScaleType("category", DatasetScale, defaultConfig); +}).call(this); + +(function() { + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + display: true, + position: "left", + id: "y-axis-1", + + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.1)", + lineWidth: 1, + drawOnChartArea: true, + drawTicks: true, // draw ticks extending towards the label + zeroLineWidth: 1, + zeroLineColor: "rgba(0,0,0,0.25)", + }, + + // scale numbers + beginAtZero: false, + override: null, + + // label settings + labels: { + show: true, + template: "<%=value.toLocaleString()%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue", + } + }; + + var LinearScale = Chart.Element.extend({ + isHorizontal: function() { + return this.options.position == "top" || this.options.position == "bottom"; + }, + generateTicks: function(width, height) { + // We need to decide how many ticks we are going to have. Each tick draws a grid line. + // There are two possibilities. The first is that the user has manually overridden the scale + // calculations in which case the job is easy. The other case is that we have to do it ourselves + // + // We assume at this point that the scale object has been updated with the following values + // by the chart. + // min: this is the minimum value of the scale + // max: this is the maximum value of the scale + // options: contains the options for the scale. This is referenced from the user settings + // rather than being cloned. This ensures that updates always propogate to a redraw + + // Reset the ticks array. Later on, we will draw a grid line at these positions + // The array simply contains the numerical value of the spots where ticks will be + this.ticks = []; + + if (this.options.override) { + // The user has specified the manual override. We use <= instead of < so that + // we get the final line + for (var i = 0; i <= this.options.override.steps; ++i) { + var value = this.options.override.start + (i * this.options.override.stepWidth); + ticks.push(value); + } + } else { + // Figure out what the max number of ticks we can support it is based on the size of + // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 + // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on + // the graph + + var maxTicks; + + if (this.isHorizontal()) { + maxTicks = Math.min(11, Math.ceil(width / 50)); + } else { + // The factor of 2 used to scale the font size has been experimentally determined. + maxTicks = Math.min(11, Math.ceil(height / (2 * this.options.labels.fontSize))); + } + + // Make sure we always have at least 2 ticks + maxTicks = Math.max(2, maxTicks); + + // To get a "nice" value for the tick spacing, we will use the appropriately named + // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks + // for details. + + // If we are forcing it to begin at 0, but 0 will already be rendered on the chart, + // do nothing since that would make the chart weird. If the user really wants a weird chart + // axis, they can manually override it + if (this.options.beginAtZero) { + var minSign = helpers.sign(this.min); + var maxSign = helpers.sign(this.max); + + if (minSign < 0 && maxSign < 0) { + // move the top up to 0 + this.max = 0; + } else if (minSign > 0 && maxSign > 0) { + // move the botttom down to 0 + this.min = 0; + } + } + + var niceRange = helpers.niceNum(this.max - this.min, false); + var spacing = helpers.niceNum(niceRange / (maxTicks - 1), true); + var niceMin = Math.floor(this.min / spacing) * spacing; + var niceMax = Math.ceil(this.max / spacing) * spacing; + + // Put the values into the ticks array + for (var j = niceMin; j <= niceMax; j += spacing) { + this.ticks.push(j); + } + } + + if (this.options.position == "left" || this.options.position == "right") { + // We are in a vertical orientation. The top value is the highest. So reverse the array + this.ticks.reverse(); + } + + // At this point, we need to update our max and min given the tick values since we have expanded the + // range of the scale + this.max = helpers.max(this.ticks); + this.min = helpers.min(this.ticks); + }, + buildLabels: function() { + // We assume that this has been run after ticks have been generated. We try to figure out + // a label for each tick. + this.labels = []; + + helpers.each(this.ticks, function(tick, index, ticks) { + var label; + + if (this.options.labels.userCallback) { + // If the user provided a callback for label generation, use that as first priority + label = this.options.labels.userCallback(tick, index, ticks); + } else if (this.options.labels.template) { + // else fall back to the template string + label = helpers.template(this.options.labels.template, { + value: tick + }); + } + + this.labels.push(label ? label : ""); // empty string will not render so we're good + }, this); + }, + // Get the correct value. If the value type is object get the x or y based on whether we are horizontal or not + getRightValue: function(rawValue) { + return typeof rawValue === "object" ? (this.isHorizontal() ? rawValue.x : rawValue.y) : rawValue; + }, + getPixelForValue: function(value) { + // This must be called after fit has been run so that + // this.left, this.top, this.right, and this.bottom have been defined + var pixel; + var range = this.max - this.min; + + if (this.isHorizontal()) { + pixel = this.left + (this.width / range * (value - this.min)); + } else { + // Bottom - top since pixels increase downard on a screen + pixel = this.bottom - (this.height / range * (value - this.min)); + } + + return pixel; + }, + + // Functions needed for line charts + calculateRange: function() { + this.min = null; + this.max = null; + + var positiveValues = []; + var negativeValues = []; + + if (this.options.stacked) { + helpers.each(this.data.datasets, function(dataset) { + if (this.isHorizontal() ? dataset.xAxisID === this.id : dataset.yAxisID === this.id) { + helpers.each(dataset.data, function(rawValue, index) { + + var value = this.getRightValue(rawValue); + + positiveValues[index] = positiveValues[index] || 0; + negativeValues[index] = negativeValues[index] || 0; + + if (this.options.relativePoints) { + positiveValues[index] = 100; + } else { + if (value < 0) { + negativeValues[index] += value; + } else { + positiveValues[index] += value; + } + } + }, this); + } + }, this); + + var values = positiveValues.concat(negativeValues); + this.min = helpers.min(values); + this.max = helpers.max(values); + + } else { + helpers.each(this.data.datasets, function(dataset) { + if (this.isHorizontal() ? dataset.xAxisID === this.id : dataset.yAxisID === this.id) { + helpers.each(dataset.data, function(rawValue, index) { + var value = this.getRightValue(rawValue); + + if (this.min === null) { + this.min = value; + } else if (value < this.min) { + this.min = value; + } + + if (this.max === null) { + this.max = value; + } else if (value > this.max) { + this.max = value; + } + }, this); + } + }, this); + } + }, + + getPointPixelForValue: function(rawValue, index, datasetIndex) { + var value = this.getRightValue(rawValue); + + if (this.options.stacked) { + var offsetPos = 0; + var offsetNeg = 0; + + for (var i = 0; i < datasetIndex; ++i) { + if (this.data.datasets[i].data[index] < 0) { + offsetNeg += this.data.datasets[i].data[index]; + } else { + offsetPos += this.data.datasets[i].data[index]; + } + } + + if (value < 0) { + return this.getPixelForValue(offsetNeg + value); + } else { + return this.getPixelForValue(offsetPos + value); + } + } else { + return this.getPixelForValue(value); + } + }, + + // Functions needed for bar charts + calculateBarBase: function(datasetIndex, index) { + var base = 0; + + if (this.options.stacked) { + + var value = this.data.datasets[datasetIndex].data[index]; + + if (value < 0) { + for (var i = 0; i < datasetIndex; i++) { + if (this.data.datasets[i].yAxisID === this.id) { + base += this.data.datasets[i].data[index] < 0 ? this.data.datasets[i].data[index] : 0; + } + } + } else { + for (var j = 0; j < datasetIndex; j++) { + if (this.data.datasets[j].yAxisID === this.id) { + base += this.data.datasets[j].data[index] > 0 ? this.data.datasets[j].data[index] : 0; + } + } + } + + return this.getPixelForValue(base); + } + + base = this.getPixelForValue(this.min); + + if (this.beginAtZero || ((this.min <= 0 && this.max >= 0) || (this.min >= 0 && this.max <= 0))) { + base = this.getPixelForValue(0); + base += this.options.gridLines.lineWidth; + } else if (this.min < 0 && this.max < 0) { + // All values are negative. Use the top as the base + base = this.getPixelForValue(this.max); + } + + return base; + + }, + calculateBarY: function(datasetIndex, index) { + var value = this.data.datasets[datasetIndex].data[index]; + + if (this.options.stacked) { + + var sumPos = 0, + sumNeg = 0; + + for (var i = 0; i < datasetIndex; i++) { + if (this.data.datasets[i].data[index] < 0) { + sumNeg += this.data.datasets[i].data[index] || 0; + } else { + sumPos += this.data.datasets[i].data[index] || 0; + } + } + + if (value < 0) { + return this.getPixelForValue(sumNeg + value); + } else { + return this.getPixelForValue(sumPos + value); + } + + return this.getPixelForValue(value); + } + + var offset = 0; + + for (var j = datasetIndex; j < this.data.datasets.length; j++) { + if (j === datasetIndex && value) { + offset += value; + } else { + offset = offset + value; + } + } + + return this.getPixelForValue(value); + }, + + // Fit this axis to the given size + // @param {number} maxWidth : the max width the axis can be + // @param {number} maxHeight: the max height the axis can be + // @return {object} minSize : the minimum size needed to draw the axis + fit: function(maxWidth, maxHeight) { + this.calculateRange(); + this.generateTicks(maxWidth, maxHeight); + this.buildLabels(); + + var minSize = { + width: 0, + height: 0, + }; + + // In a horizontal axis, we need some room for the scale to be drawn + // + // ----------------------------------------------------- + // | | | | | + // + // In a vertical axis, we need some room for the scale to be drawn. + // The actual grid lines will be drawn on the chart area, however, we need to show + // ticks where the axis actually is. + // We will allocate 25px for this width + // | + // -| + // | + // | + // -| + // | + // | + // -| - // 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 = helpers.min([(this.height / 2 - this.options.pointLabels.fontSize - 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 = helpers.fontString(this.options.pointLabels.fontSize, this.options.pointLabels.fontStyle, this.options.pointLabels.fontFamily); - for (i = 0; i < this.valuesCount; i++) { - // 5px to space the text slightly out - similar to what we do in the draw function. - pointPosition = this.getPointPosition(i, largestPossibleRadius); - textWidth = this.ctx.measureText(helpers.template(this.options.labels.template, { - value: this.labels[i] - })).width + 5; - if (i === 0 || i === this.valuesCount / 2) { - // If we're at index zero, or exactly the middle, we're at exactly the top/bottom - // of the radar chart, so text will be aligned centrally, so we'll half it and compare - // w/left and right text sizes - halfTextWidth = textWidth / 2; - if (pointPosition.x + halfTextWidth > 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; - } - } - } + // Width + if (this.isHorizontal()) { + minSize.width = maxWidth; // fill all the width + } else { + minSize.width = this.options.gridLines.show && this.options.display ? 10 : 0; + } - xProtrusionLeft = furthestLeft; + // height + if (this.isHorizontal()) { + minSize.height = this.options.gridLines.show && this.options.display ? 10 : 0; + } else { + minSize.height = maxHeight; // fill all the height + } - xProtrusionRight = Math.ceil(furthestRight - this.width); - furthestRightAngle = this.getIndexAngle(furthestRightIndex); - furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); + if (this.options.labels.show && this.options.display) { + // Don't bother fitting the labels if we are not showing them + var labelFont = helpers.fontString(this.options.labels.fontSize, + this.options.labels.fontStyle, this.options.labels.fontFamily); - radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI / 2); + if (this.isHorizontal()) { + // A horizontal axis is more constrained by the height. + var maxLabelHeight = maxHeight - minSize.height; + var labelHeight = 1.5 * this.options.labels.fontSize; + minSize.height = Math.min(maxHeight, minSize.height + labelHeight); + } else { + // A vertical axis is more constrained by the width. Labels are the dominant factor + // here, so get that length first + var maxLabelWidth = maxWidth - minSize.width; + var largestTextWidth = helpers.longestText(this.ctx, labelFont, this.labels); - radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI / 2); + if (largestTextWidth < maxLabelWidth) { + // We don't need all the room + minSize.width += largestTextWidth; + } else { + // Expand to max size + minSize.width = maxWidth; + } + } + } - // Ensure we actually need to reduce the size of the chart - radiusReductionRight = (helpers.isNumber(radiusReductionRight)) ? radiusReductionRight : 0; - radiusReductionLeft = (helpers.isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; + this.width = minSize.width; + this.height = minSize.height; + return minSize; + }, + // Actualy draw the scale on the canvas + // @param {rectangle} chartArea : the area of the chart to draw full grid lines on + draw: function(chartArea) { + if (this.options.display) { - this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2; + var setContextLineSettings; + var hasZero; - //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) - this.setCenterPoint(radiusReductionLeft, radiusReductionRight); + // Make sure we draw text in the correct color + this.ctx.fillStyle = this.options.labels.fontColor; - }, - setCenterPoint: function(leftMovement, rightMovement) { + if (this.isHorizontal()) { + if (this.options.gridLines.show) { + // Draw the horizontal line + setContextLineSettings = true; + hasZero = helpers.findNextWhere(this.ticks, function(tick) { + return tick === 0; + }) !== undefined; + var yTickStart = this.options.position == "bottom" ? this.top : this.bottom - 5; + var yTickEnd = this.options.position == "bottom" ? this.top + 5 : this.bottom; - var maxRight = this.width - rightMovement - this.drawingArea, - maxLeft = leftMovement + this.drawingArea; + helpers.each(this.ticks, function(tick, index) { + // Grid lines are vertical + var xValue = this.getPixelForValue(tick); - this.xCenter = (maxLeft + maxRight) / 2; - // Always vertically in the centre as the text height doesn't change - this.yCenter = (this.height / 2); - }, + if (tick === 0 || (!hasZero && index === 0)) { + // Draw the 0 point specially or the left if there is no 0 + this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; + this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; + setContextLineSettings = true; // reset next time + } else if (setContextLineSettings) { + this.ctx.lineWidth = this.options.gridLines.lineWidth; + this.ctx.strokeStyle = this.options.gridLines.color; + setContextLineSettings = false; + } - getIndexAngle: function(index) { - var angleMultiplier = (Math.PI * 2) / this.valuesCount; - // Start from the top instead of right, so remove a quarter of the circle + xValue += helpers.aliasPixel(this.ctx.lineWidth); - 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.options.display) { - var ctx = this.ctx; - helpers.each(this.yLabels, function(label, index) { - // Don't draw a centre value - if (index > 0) { - var yCenterOffset = index * (this.drawingArea / Math.max(this.ticks.length, 1)), - yHeight = this.yCenter - yCenterOffset, - pointPosition; + // Draw the label area + this.ctx.beginPath(); - // Draw circular lines around the scale - if (this.options.gridLines.show) { - ctx.strokeStyle = this.options.gridLines.color; - ctx.lineWidth = this.options.gridLines.lineWidth; + if (this.options.gridLines.drawTicks) { + this.ctx.moveTo(xValue, yTickStart); + this.ctx.lineTo(xValue, yTickEnd); + } - if (this.options.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 < this.valuesCount; i++) { - pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.ticks[index])); - if (i === 0) { - ctx.moveTo(pointPosition.x, pointPosition.y); - } else { - ctx.lineTo(pointPosition.x, pointPosition.y); - } - } - ctx.closePath(); - ctx.stroke(); - } - } + // Draw the chart area + if (this.options.gridLines.drawOnChartArea) { + this.ctx.moveTo(xValue, chartArea.top); + this.ctx.lineTo(xValue, chartArea.bottom); + } - if (this.options.labels.show) { - ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + // Need to stroke in the loop because we are potentially changing line widths & colours + this.ctx.stroke(); + }, this); + } - if (this.showLabelBackdrop) { - var labelWidth = ctx.measureText(label).width; - ctx.fillStyle = this.options.labels.backdropColor; - ctx.fillRect( - this.xCenter - labelWidth / 2 - this.options.labels.backdropPaddingX, - yHeight - this.fontSize / 2 - this.options.labels.backdropPaddingY, - labelWidth + this.options.labels.backdropPaddingX * 2, - this.options.labels.fontSize + this.options.lables.backdropPaddingY * 2 - ); - } + if (this.options.labels.show) { + // Draw the labels - ctx.textAlign = 'center'; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.options.labels.fontColor; - ctx.fillText(label, this.xCenter, yHeight); - } - } - }, this); + var labelStartY; - if (!this.options.lineArc) { - ctx.lineWidth = this.options.angleLines.lineWidth; - ctx.strokeStyle = this.options.angleLines.color; + if (this.options.position == "top") { + labelStartY = this.bottom - 10; + this.ctx.textBaseline = "bottom"; + } else { + // bottom side + labelStartY = this.top + 10; + this.ctx.textBaseline = "top"; + } - for (var i = this.valuesCount - 1; i >= 0; i--) { - if (this.options.angleLines.show) { - 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 = helpers.fontString(this.options.pointLabels.fontSize, this.options.pointLabels.fontStyle, this.options.pointLabels.fontFamily); - ctx.fillStyle = this.options.pointLabels.fontColor; + this.ctx.textAlign = "center"; + this.ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); - 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'; - } + helpers.each(this.labels, function(label, index) { + var xValue = this.getPixelForValue(this.ticks[index]); + this.ctx.fillText(label, xValue, labelStartY); + }, this); + } + } else { + // Vertical + if (this.options.gridLines.show) { - // Set the correct text baseline based on outer positioning - if (exactQuarter) { - ctx.textBaseline = 'middle'; - } else if (upperHalf) { - ctx.textBaseline = 'bottom'; - } else { - ctx.textBaseline = 'top'; - } + // Draw the vertical line + setContextLineSettings = true; + hasZero = helpers.findNextWhere(this.ticks, function(tick) { + return tick === 0; + }) !== undefined; + var xTickStart = this.options.position == "right" ? this.left : this.right - 5; + var xTickEnd = this.options.position == "right" ? this.left + 5 : this.right; - ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); - } - } - } - } - }); - Chart.scales.registerScaleType("radialLinear", LinearRadialScale); + helpers.each(this.ticks, function(tick, index) { + // Grid lines are horizontal + var yValue = this.getPixelForValue(tick); + + if (tick === 0 || (!hasZero && index === 0)) { + // Draw the 0 point specially or the bottom if there is no 0 + this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; + this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; + setContextLineSettings = true; // reset next time + } else if (setContextLineSettings) { + this.ctx.lineWidth = this.options.gridLines.lineWidth; + this.ctx.strokeStyle = this.options.gridLines.color; + setContextLineSettings = false; // use boolean to indicate that we only want to do this once + } + + yValue += helpers.aliasPixel(this.ctx.lineWidth); + + // Draw the label area + this.ctx.beginPath(); + + if (this.options.gridLines.drawTicks) { + this.ctx.moveTo(xTickStart, yValue); + this.ctx.lineTo(xTickEnd, yValue); + } + + // Draw the chart area + if (this.options.gridLines.drawOnChartArea) { + this.ctx.moveTo(chartArea.left, yValue); + this.ctx.lineTo(chartArea.right, yValue); + } + + this.ctx.stroke(); + }, this); + } + + if (this.options.labels.show) { + // Draw the labels + + var labelStartX; + + if (this.options.position == "left") { + labelStartX = this.right - 10; + this.ctx.textAlign = "right"; + } else { + // right side + labelStartX = this.left + 5; + this.ctx.textAlign = "left"; + } + + this.ctx.textBaseline = "middle"; + this.ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + + helpers.each(this.labels, function(label, index) { + var yValue = this.getPixelForValue(this.ticks[index]); + this.ctx.fillText(label, labelStartX, yValue); + }, this); + } + } + } + } + }); + Chart.scaleService.registerScaleType("linear", LinearScale, defaultConfig); + +}).call(this); + +(function() { + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + display: true, + + //Boolean - Whether to animate scaling the chart from the centre + animate: false, + + lineArc: false, + + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.1)", + lineWidth: 1, + }, + + angleLines: { + show: true, + color: "rgba(0,0,0, 0.1)", + lineWidth: 1 + }, + + // scale numbers + beginAtZero: true, + + // label settings + labels: { + show: true, + template: "<%=value.toLocaleString()%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue", + + //Boolean - Show a backdrop to the scale label + showLabelBackdrop: true, + + //String - The colour of the label backdrop + backdropColor: "rgba(255,255,255,0.75)", + + //Number - The backdrop padding above & below the label in pixels + backdropPaddingY: 2, + + //Number - The backdrop padding to the side of the label in pixels + backdropPaddingX: 2, + }, + + pointLabels: { + //String - Point label font declaration + fontFamily: "'Arial'", + + //String - Point label font weight + fontStyle: "normal", + + //Number - Point label font size in pixels + fontSize: 10, + + //String - Point label font colour + fontColor: "#666", + }, + }; + + var LinearRadialScale = Chart.Element.extend({ + initialize: function() { + this.size = helpers.min([this.height, this.width]); + this.drawingArea = (this.options.display) ? (this.size / 2) - (this.options.labels.fontSize / 2 + this.options.labels.backdropPaddingY) : (this.size / 2); + }, + update: function() { + if (!this.options.lineArc) { + this.setScaleSize(); + } else { + this.drawingArea = (this.options.display) ? (this.size / 2) - (this.fontSize / 2 + this.backdropPaddingY) : (this.size / 2); + } + + this.buildYLabels(); + }, + calculateRange: function() { + this.min = null; + this.max = null; + + helpers.each(this.data.datasets, function(dataset) { + helpers.each(dataset.data, function(value, index) { + if (this.min === null) { + this.min = value; + } else if (value < this.min) { + this.min = value; + } + + if (this.max === null) { + this.max = value; + } else if (value > this.max) { + this.max = value; + } + }, this); + }, this); + }, + generateTicks: function() { + // We need to decide how many ticks we are going to have. Each tick draws a grid line. + // There are two possibilities. The first is that the user has manually overridden the scale + // calculations in which case the job is easy. The other case is that we have to do it ourselves + // + // We assume at this point that the scale object has been updated with the following values + // by the chart. + // min: this is the minimum value of the scale + // max: this is the maximum value of the scale + // options: contains the options for the scale. This is referenced from the user settings + // rather than being cloned. This ensures that updates always propogate to a redraw + + // Reset the ticks array. Later on, we will draw a grid line at these positions + // The array simply contains the numerical value of the spots where ticks will be + this.ticks = []; + + if (this.options.override) { + // The user has specified the manual override. We use <= instead of < so that + // we get the final line + for (var i = 0; i <= this.options.override.steps; ++i) { + var value = this.options.override.start + (i * this.options.override.stepWidth); + ticks.push(value); + } + } else { + // Figure out what the max number of ticks we can support it is based on the size of + // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 + // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on + // the graph + + var maxTicks = Math.min(11, Math.ceil(this.drawingArea / (2 * this.options.labels.fontSize))); + + // Make sure we always have at least 2 ticks + maxTicks = Math.max(2, maxTicks); + + // To get a "nice" value for the tick spacing, we will use the appropriately named + // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks + // for details. + + // If we are forcing it to begin at 0, but 0 will already be rendered on the chart, + // do nothing since that would make the chart weird. If the user really wants a weird chart + // axis, they can manually override it + if (this.options.beginAtZero) { + var minSign = helpers.sign(this.min); + var maxSign = helpers.sign(this.max); + + if (minSign < 0 && maxSign < 0) { + // move the top up to 0 + this.max = 0; + } else if (minSign > 0 && maxSign > 0) { + // move the botttom down to 0 + this.min = 0; + } + } + + var niceRange = helpers.niceNum(this.max - this.min, false); + var spacing = helpers.niceNum(niceRange / (maxTicks - 1), true); + var niceMin = Math.floor(this.min / spacing) * spacing; + var niceMax = Math.ceil(this.max / spacing) * spacing; + + // Put the values into the ticks array + for (var j = niceMin; j <= niceMax; j += spacing) { + this.ticks.push(j); + } + } + + if (this.options.position == "left" || this.options.position == "right") { + // We are in a vertical orientation. The top value is the highest. So reverse the array + this.ticks.reverse(); + } + + // At this point, we need to update our max and min given the tick values since we have expanded the + // range of the scale + this.max = helpers.max(this.ticks); + this.min = helpers.min(this.ticks); + }, + buildYLabels: function() { + this.yLabels = []; + + helpers.each(this.ticks, function(tick, index, ticks) { + var label; + + if (this.options.labels.userCallback) { + // If the user provided a callback for label generation, use that as first priority + label = this.options.labels.userCallback(tick, index, ticks); + } else if (this.options.labels.template) { + // else fall back to the template string + label = helpers.template(this.options.labels.template, { + value: tick + }); + } + + this.yLabels.push(label ? label : ""); + }, this); + }, + 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 = helpers.min([(this.height / 2 - this.options.pointLabels.fontSize - 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 = helpers.fontString(this.options.pointLabels.fontSize, this.options.pointLabels.fontStyle, this.options.pointLabels.fontFamily); + for (i = 0; i < this.valuesCount; i++) { + // 5px to space the text slightly out - similar to what we do in the draw function. + pointPosition = this.getPointPosition(i, largestPossibleRadius); + textWidth = this.ctx.measureText(helpers.template(this.options.labels.template, { + value: this.labels[i] + })).width + 5; + if (i === 0 || i === this.valuesCount / 2) { + // If we're at index zero, or exactly the middle, we're at exactly the top/bottom + // of the radar chart, so text will be aligned centrally, so we'll half it and compare + // w/left and right text sizes + halfTextWidth = textWidth / 2; + if (pointPosition.x + halfTextWidth > 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 = (helpers.isNumber(radiusReductionRight)) ? radiusReductionRight : 0; + radiusReductionLeft = (helpers.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); + }, + getDistanceFromCenterForValue: 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; + }, + getPointPosition: function(index, distanceFromCenter) { + var thisAngle = this.getIndexAngle(index); + return { + x: (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, + y: (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter + }; + }, + getPointPositionForValue: function(index, value) { + return this.getPointPosition(index, this.getDistanceFromCenterForValue(value)); + }, + draw: function() { + if (this.options.display) { + var ctx = this.ctx; + helpers.each(this.yLabels, function(label, index) { + // Don't draw a centre value + if (index > 0) { + var yCenterOffset = this.getDistanceFromCenterForValue(this.ticks[index]); + var yHeight = this.yCenter - yCenterOffset; + + // Draw circular lines around the scale + if (this.options.gridLines.show) { + ctx.strokeStyle = this.options.gridLines.color; + ctx.lineWidth = this.options.gridLines.lineWidth; + + if (this.options.lineArc) { + // Draw circular arcs between the points + ctx.beginPath(); + ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI * 2); + ctx.closePath(); + ctx.stroke(); + } else { + // Draw straight lines connecting each index + ctx.beginPath(); + for (var i = 0; i < this.valuesCount; i++) { + var pointPosition = this.getPointPosition(i, this.getDistanceFromCenterForValue(this.ticks[index])); + if (i === 0) { + ctx.moveTo(pointPosition.x, pointPosition.y); + } else { + ctx.lineTo(pointPosition.x, pointPosition.y); + } + } + ctx.closePath(); + ctx.stroke(); + } + } + + if (this.options.labels.show) { + ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + + if (this.options.labels.showLabelBackdrop) { + var labelWidth = ctx.measureText(label).width; + ctx.fillStyle = this.options.labels.backdropColor; + ctx.fillRect( + this.xCenter - labelWidth / 2 - this.options.labels.backdropPaddingX, + yHeight - this.fontSize / 2 - this.options.labels.backdropPaddingY, + labelWidth + this.options.labels.backdropPaddingX * 2, + this.options.labels.fontSize + this.options.labels.backdropPaddingY * 2 + ); + } + + ctx.textAlign = 'center'; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.options.labels.fontColor; + ctx.fillText(label, this.xCenter, yHeight); + } + } + }, this); + + if (!this.options.lineArc) { + ctx.lineWidth = this.options.angleLines.lineWidth; + ctx.strokeStyle = this.options.angleLines.color; + + for (var i = this.valuesCount - 1; i >= 0; i--) { + if (this.options.angleLines.show) { + var outerPosition = this.getPointPosition(i, this.getDistanceFromCenterForValue(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.getDistanceFromCenterForValue(this.max) + 5); + ctx.font = helpers.fontString(this.options.pointLabels.fontSize, this.options.pointLabels.fontStyle, this.options.pointLabels.fontFamily); + ctx.fillStyle = this.options.pointLabels.fontColor; + + 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); + } + } + } + } + }); + Chart.scaleService.registerScaleType("radialLinear", LinearRadialScale, defaultConfig); }).call(this); @@ -2922,83 +4356,83 @@ (function() { - "use strict"; + "use strict"; - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; - Chart.defaults.global.elements.arc = { - backgroundColor: Chart.defaults.global.defaultColor, - borderColor: "#fff", - borderWidth: 2 - }; + Chart.defaults.global.elements.arc = { + backgroundColor: Chart.defaults.global.defaultColor, + borderColor: "#fff", + borderWidth: 2 + }; - Chart.Arc = Chart.Element.extend({ - inGroupRange: function(mouseX) { - var vm = this._view; + Chart.Arc = Chart.Element.extend({ + inGroupRange: function(mouseX) { + var vm = this._view; - if (vm) { - return (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hoverRadius, 2)); - } else { - return false; - } - }, - inRange: function(chartX, chartY) { + if (vm) { + return (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hoverRadius, 2)); + } else { + return false; + } + }, + inRange: function(chartX, chartY) { - var vm = this._view; + var vm = this._view; - var pointRelativePosition = helpers.getAngleFromPoint(vm, { - x: chartX, - y: chartY - }); + var pointRelativePosition = helpers.getAngleFromPoint(vm, { + x: chartX, + y: chartY + }); - // Put into the range of (-PI/2, 3PI/2] - var startAngle = vm.startAngle < (-0.5 * Math.PI) ? vm.startAngle + (2.0 * Math.PI) : vm.startAngle > (1.5 * Math.PI) ? vm.startAngle - (2.0 * Math.PI) : vm.startAngle; - var endAngle = vm.endAngle < (-0.5 * Math.PI) ? vm.endAngle + (2.0 * Math.PI) : vm.endAngle > (1.5 * Math.PI) ? vm.endAngle - (2.0 * Math.PI) : vm.endAngle + // Put into the range of (-PI/2, 3PI/2] + var startAngle = vm.startAngle < (-0.5 * Math.PI) ? vm.startAngle + (2.0 * Math.PI) : vm.startAngle > (1.5 * Math.PI) ? vm.startAngle - (2.0 * Math.PI) : vm.startAngle; + var endAngle = vm.endAngle < (-0.5 * Math.PI) ? vm.endAngle + (2.0 * Math.PI) : vm.endAngle > (1.5 * Math.PI) ? vm.endAngle - (2.0 * Math.PI) : vm.endAngle - //Check if within the range of the open/close angle - var betweenAngles = (pointRelativePosition.angle >= startAngle && pointRelativePosition.angle <= endAngle), - withinRadius = (pointRelativePosition.distance >= vm.innerRadius && pointRelativePosition.distance <= vm.outerRadius); + //Check if within the range of the open/close angle + var betweenAngles = (pointRelativePosition.angle >= startAngle && pointRelativePosition.angle <= endAngle), + withinRadius = (pointRelativePosition.distance >= vm.innerRadius && pointRelativePosition.distance <= vm.outerRadius); - return (betweenAngles && withinRadius); - //Ensure within the outside of the arc centre, but inside arc outer - }, - tooltipPosition: function() { - var vm = this._view; + return (betweenAngles && withinRadius); + //Ensure within the outside of the arc centre, but inside arc outer + }, + tooltipPosition: function() { + var vm = this._view; - var centreAngle = vm.startAngle + ((vm.endAngle - vm.startAngle) / 2), - rangeFromCentre = (vm.outerRadius - vm.innerRadius) / 2 + vm.innerRadius; - return { - x: vm.x + (Math.cos(centreAngle) * rangeFromCentre), - y: vm.y + (Math.sin(centreAngle) * rangeFromCentre) - }; - }, - draw: function() { + var centreAngle = vm.startAngle + ((vm.endAngle - vm.startAngle) / 2), + rangeFromCentre = (vm.outerRadius - vm.innerRadius) / 2 + vm.innerRadius; + return { + x: vm.x + (Math.cos(centreAngle) * rangeFromCentre), + y: vm.y + (Math.sin(centreAngle) * rangeFromCentre) + }; + }, + draw: function() { - var ctx = this._chart.ctx; - var vm = this._view; + var ctx = this._chart.ctx; + var vm = this._view; - ctx.beginPath(); + ctx.beginPath(); - ctx.arc(vm.x, vm.y, vm.outerRadius, vm.startAngle, vm.endAngle); + ctx.arc(vm.x, vm.y, vm.outerRadius, vm.startAngle, vm.endAngle); - ctx.arc(vm.x, vm.y, vm.innerRadius, vm.endAngle, vm.startAngle, true); + ctx.arc(vm.x, vm.y, vm.innerRadius, vm.endAngle, vm.startAngle, true); - ctx.closePath(); - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; + ctx.closePath(); + ctx.strokeStyle = vm.borderColor; + ctx.lineWidth = vm.borderWidth; - ctx.fillStyle = vm.backgroundColor; + ctx.fillStyle = vm.backgroundColor; - ctx.fill(); - ctx.lineJoin = 'bevel'; + ctx.fill(); + ctx.lineJoin = 'bevel'; - if (vm.borderWidth) { - ctx.stroke(); - } - } - }); + if (vm.borderWidth) { + ctx.stroke(); + } + } + }); }).call(this); @@ -3016,181 +4450,169 @@ (function() { - "use strict"; + "use strict"; - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; - Chart.defaults.global.elements.line = { - tension: 0.4, - backgroundColor: Chart.defaults.global.defaultColor, - borderWidth: 3, - borderColor: Chart.defaults.global.defaultColor, - fill: true, // do we fill in the area between the line and its base axis - skipNull: true, - drawNull: false, - }; + Chart.defaults.global.elements.line = { + tension: 0.4, + backgroundColor: Chart.defaults.global.defaultColor, + borderWidth: 3, + borderColor: Chart.defaults.global.defaultColor, + fill: true, // do we fill in the area between the line and its base axis + skipNull: true, + drawNull: false, + }; - Chart.Line = Chart.Element.extend({ - draw: function() { + Chart.elements.Line = Chart.Element.extend({ + draw: function() { - var vm = this._view; - var ctx = this._chart.ctx; - var first = this._children[0]; - var last = this._children[this._children.length - 1]; + var vm = this._view; + var ctx = this._chart.ctx; + var first = this._children[0]; + var last = this._children[this._children.length - 1]; - // Draw the background first (so the border is always on top) - helpers.each(this._children, function(point, index) { - var previous = this.previousPoint(point, this._children, index); - var next = this.nextPoint(point, this._children, index); + // Draw the background first (so the border is always on top) + helpers.each(this._children, function(point, index) { + var previous = helpers.previousItem(this._children, index); + var next = helpers.nextItem(this._children, index); - // First point only - if (index === 0) { - ctx.moveTo(point._view.x, point._view.y); - return; - } + // First point only + if (index === 0) { + ctx.moveTo(point._view.x, point._view.y); + return; + } - // Start Skip and drag along scale baseline - if (point._view.skip && vm.skipNull && !this._loop) { - ctx.lineTo(previous._view.x, point._view.y); - ctx.moveTo(next._view.x, point._view.y); - } - // End Skip Stright line from the base line - else if (previous._view.skip && vm.skipNull && !this._loop) { - ctx.moveTo(point._view.x, previous._view.y); - ctx.lineTo(point._view.x, point._view.y); - } + // Start Skip and drag along scale baseline + if (point._view.skip && vm.skipNull && !this._loop) { + ctx.lineTo(previous._view.x, point._view.y); + ctx.moveTo(next._view.x, point._view.y); + } + // End Skip Stright line from the base line + else if (previous._view.skip && vm.skipNull && !this._loop) { + ctx.moveTo(point._view.x, previous._view.y); + ctx.lineTo(point._view.x, point._view.y); + } - if (previous._view.skip && vm.skipNull) { - ctx.moveTo(point._view.x, point._view.y); - } - // Normal Bezier Curve - else { - if (vm.tension > 0) { - ctx.bezierCurveTo( - previous._view.controlPointNextX, - previous._view.controlPointNextY, - point._view.controlPointPreviousX, - point._view.controlPointPreviousY, - point._view.x, - point._view.y - ); - } else { - ctx.lineTo(point._view.x, point._view.y); - } - } - }, this); + if (previous._view.skip && vm.skipNull) { + ctx.moveTo(point._view.x, point._view.y); + } + // Normal Bezier Curve + else { + if (vm.tension > 0) { + ctx.bezierCurveTo( + previous._view.controlPointNextX, + previous._view.controlPointNextY, + point._view.controlPointPreviousX, + point._view.controlPointPreviousY, + point._view.x, + point._view.y + ); + } else { + ctx.lineTo(point._view.x, point._view.y); + } + } + }, this); - // For radial scales, loop back around to the first point - if (this._loop) { - if (vm.tension > 0 && !first._view.skip) { + // For radial scales, loop back around to the first point + if (this._loop) { + if (vm.tension > 0 && !first._view.skip) { - ctx.bezierCurveTo( - last._view.controlPointNextX, - last._view.controlPointNextY, - first._view.controlPointPreviousX, - first._view.controlPointPreviousY, - first._view.x, - first._view.y - ); - } else { - ctx.lineTo(first._view.x, first._view.y); - } - } + ctx.bezierCurveTo( + last._view.controlPointNextX, + last._view.controlPointNextY, + first._view.controlPointPreviousX, + first._view.controlPointPreviousY, + first._view.x, + first._view.y + ); + } else { + ctx.lineTo(first._view.x, first._view.y); + } + } - // If we had points and want to fill this line, do so. - if (this._children.length > 0 && vm.fill) { - //Round off the line by going to the base of the chart, back to the start, then fill. - ctx.lineTo(this._children[this._children.length - 1]._view.x, vm.scaleZero); - ctx.lineTo(this._children[0]._view.x, vm.scaleZero); - ctx.fillStyle = vm.backgroundColor || Chart.defaults.global.defaultColor; - ctx.closePath(); - ctx.fill(); - } + // If we had points and want to fill this line, do so. + if (this._children.length > 0 && vm.fill) { + //Round off the line by going to the base of the chart, back to the start, then fill. + ctx.lineTo(this._children[this._children.length - 1]._view.x, vm.scaleZero); + ctx.lineTo(this._children[0]._view.x, vm.scaleZero); + ctx.fillStyle = vm.backgroundColor || Chart.defaults.global.defaultColor; + ctx.closePath(); + ctx.fill(); + } - // Now draw the line between all the points with any borders - ctx.lineWidth = vm.borderWidth || Chart.defaults.global.defaultColor; - ctx.strokeStyle = vm.borderColor || Chart.defaults.global.defaultColor; - ctx.beginPath(); + // Now draw the line between all the points with any borders + ctx.lineWidth = vm.borderWidth || Chart.defaults.global.defaultColor; + ctx.strokeStyle = vm.borderColor || Chart.defaults.global.defaultColor; + ctx.beginPath(); - helpers.each(this._children, function(point, index) { - var previous = this.previousPoint(point, this._children, index); - var next = this.nextPoint(point, this._children, index); + helpers.each(this._children, function(point, index) { + var previous = helpers.previousItem(this._children, index); + var next = helpers.nextItem(this._children, index); - // First point only - if (index === 0) { - ctx.moveTo(point._view.x, point._view.y); - return; - } + // First point only + if (index === 0) { + ctx.moveTo(point._view.x, point._view.y); + return; + } - // Start Skip and drag along scale baseline - if (point._view.skip && vm.skipNull && !this._loop) { - ctx.moveTo(previous._view.x, point._view.y); - ctx.moveTo(next._view.x, point._view.y); - return; - } - // End Skip Stright line from the base line - if (previous._view.skip && vm.skipNull && !this._loop) { - ctx.moveTo(point._view.x, previous._view.y); - ctx.moveTo(point._view.x, point._view.y); - return; - } + // Start Skip and drag along scale baseline + if (point._view.skip && vm.skipNull && !this._loop) { + ctx.moveTo(previous._view.x, point._view.y); + ctx.moveTo(next._view.x, point._view.y); + return; + } + // End Skip Stright line from the base line + if (previous._view.skip && vm.skipNull && !this._loop) { + ctx.moveTo(point._view.x, previous._view.y); + ctx.moveTo(point._view.x, point._view.y); + return; + } - if (previous._view.skip && vm.skipNull) { - ctx.moveTo(point._view.x, point._view.y); - return; - } - // Normal Bezier Curve - if (vm.tension > 0) { - ctx.bezierCurveTo( - previous._view.controlPointNextX, - previous._view.controlPointNextY, - point._view.controlPointPreviousX, - point._view.controlPointPreviousY, - point._view.x, - point._view.y - ); - } else { - ctx.lineTo(point._view.x, point._view.y); - } - }, this); + if (previous._view.skip && vm.skipNull) { + ctx.moveTo(point._view.x, point._view.y); + return; + } + // Normal Bezier Curve + if (vm.tension > 0) { + ctx.bezierCurveTo( + previous._view.controlPointNextX, + previous._view.controlPointNextY, + point._view.controlPointPreviousX, + point._view.controlPointPreviousY, + point._view.x, + point._view.y + ); + } else { + ctx.lineTo(point._view.x, point._view.y); + } + }, this); - if (this._loop && !first._view.skip) { - if (vm.tension > 0) { + if (this._loop && !first._view.skip) { + if (vm.tension > 0) { - ctx.bezierCurveTo( - last._view.controlPointNextX, - last._view.controlPointNextY, - first._view.controlPointPreviousX, - first._view.controlPointPreviousY, - first._view.x, - first._view.y - ); - } else { - ctx.lineTo(first._view.x, first._view.y); - } - } + ctx.bezierCurveTo( + last._view.controlPointNextX, + last._view.controlPointNextY, + first._view.controlPointPreviousX, + first._view.controlPointPreviousY, + first._view.x, + first._view.y + ); + } else { + ctx.lineTo(first._view.x, first._view.y); + } + } - ctx.stroke(); + ctx.stroke(); - }, - nextPoint: function(point, collection, index) { - if (this.loop) { - return collection[index + 1] || collection[0]; - } - return collection[index + 1] || collection[collection.length - 1]; - }, - previousPoint: function(point, collection, index) { - if (this.loop) { - return collection[index - 1] || collection[collection.length - 1]; - } - return collection[index - 1] || collection[0]; - }, - }); + }, + }); }).call(this); @@ -3207,3251 +4629,237 @@ (function() { - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - Chart.defaults.global.elements.point = { - radius: 3, - backgroundColor: Chart.defaults.global.defaultColor, - borderWidth: 1, - borderColor: Chart.defaults.global.defaultColor, - // Hover - hitRadius: 1, - hoverRadius: 4, - hoverBorderWidth: 1, - }; - - - Chart.Point = Chart.Element.extend({ - inRange: function(mouseX, mouseY) { - var vm = this._view; - var hoverRange = vm.hitRadius + vm.radius; - return ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(hoverRange, 2)); - }, - inGroupRange: function(mouseX) { - var vm = this._view; - - if (vm) { - return (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)); - } else { - return false; - } - }, - tooltipPosition: function() { - var vm = this._view; - return { - x: vm.x, - y: vm.y, - padding: vm.radius + vm.borderWidth - }; - }, - draw: function() { - - var vm = this._view; - var ctx = this._chart.ctx; - - - if (vm.skip) { - return; - } - - if (vm.radius > 0 || vm.borderWidth > 0) { - - ctx.beginPath(); - - ctx.arc(vm.x, vm.y, vm.radius || Chart.defaults.global.elements.point.radius, 0, Math.PI * 2); - ctx.closePath(); - - ctx.strokeStyle = vm.borderColor || Chart.defaults.global.defaultColor; - ctx.lineWidth = vm.borderWidth || Chart.defaults.global.elements.point.borderWidth; - - ctx.fillStyle = vm.backgroundColor || Chart.defaults.global.defaultColor; - - ctx.fill(); - ctx.stroke(); - } - } - }); - - -}).call(this); - -/*! - * Chart.js - * http://chartjs.org/ - * Version: 2.0.0-alpha - * - * Copyright 2015 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ - - -(function() { - - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - Chart.defaults.global.elements.rectangle = { - backgroundColor: Chart.defaults.global.defaultColor, - borderWidth: 0, - borderColor: Chart.defaults.global.defaultColor, - }; - - Chart.Rectangle = Chart.Element.extend({ - draw: function() { - - var ctx = this._chart.ctx; - var vm = this._view; - - var halfWidth = vm.width / 2, - leftX = vm.x - halfWidth, - rightX = vm.x + halfWidth, - top = vm.base - (vm.base - vm.y), - halfStroke = vm.borderWidth / 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 (vm.borderWidth) { - leftX += halfStroke; - rightX -= halfStroke; - top += halfStroke; - } - - ctx.beginPath(); - - ctx.fillStyle = vm.backgroundColor; - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; - - // It'd be nice to keep this class totally generic to any rectangle - // and simply specify which border to miss out. - ctx.moveTo(leftX, vm.base); - ctx.lineTo(leftX, top); - ctx.lineTo(rightX, top); - ctx.lineTo(rightX, vm.base); - ctx.fill(); - if (vm.borderWidth) { - ctx.stroke(); - } - }, - height: function() { - var vm = this._view; - return vm.base - vm.y; - }, - inRange: function(mouseX, mouseY) { - var vm = this._view; - if (vm.y < vm.base) { - return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.y && mouseY <= vm.base); - } else { - return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.base && mouseY <= vm.y); - } - }, - inGroupRange: function(mouseX) { - var vm = this._view; - return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2); - }, - tooltipPosition: function() { - var vm = this._view; - if (vm.y < vm.base) { - return { - x: vm.x, - y: vm.y - }; - } else { - return { - x: vm.x, - y: vm.base - }; - } - }, - }); - -}).call(this); - -(function() { - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - var defaultConfig = { - - stacked: false, - valueSpacing: 5, - datasetSpacing: 1, - - hover: { - mode: "label" - }, - - scales: { - xAxes: [{ - type: "category", // scatter should not use a dataset axis - display: true, - position: "bottom", - id: "x-axis-1", // need an ID so datasets can reference the scale - - // grid line settings - gridLines: { - show: true, - color: "rgba(0, 0, 0, 0.05)", - lineWidth: 1, - drawOnChartArea: true, - drawTicks: true, - zeroLineWidth: 1, - zeroLineColor: "rgba(0,0,0,0.25)", - offsetGridLines: true, - }, - - // label settings - labels: { - show: true, - template: "<%=value%>", - fontSize: 12, - fontStyle: "normal", - fontColor: "#666", - fontFamily: "Helvetica Neue", - }, - }], - yAxes: [{ - type: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance - display: true, - position: "left", - id: "y-axis-1", - - // grid line settings - gridLines: { - show: true, - color: "rgba(0, 0, 0, 0.05)", - lineWidth: 1, - drawOnChartArea: true, - drawTicks: true, // draw ticks extending towards the label - zeroLineWidth: 1, - zeroLineColor: "rgba(0,0,0,0.25)", - }, - - // scale numbers - beginAtZero: false, - override: null, - - // label settings - labels: { - show: true, - template: "<%=value%>", - fontSize: 12, - fontStyle: "normal", - fontColor: "#666", - fontFamily: "Helvetica Neue", - } - }], - }, - - }; - - - Chart.Type.extend({ - name: "Bar", - defaults: defaultConfig, - initialize: function() { - - var _this = this; - - // Events - helpers.bindEvents(this, this.options.events, this.events); - - //Create a new bar for each piece of data - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - dataset.metaData = []; - helpers.each(dataset.data, function(dataPoint, index) { - dataset.metaData.push(new Chart.Rectangle({ - _chart: this.chart, - _datasetIndex: datasetIndex, - _index: index, - })); - }, this); - - // The bar chart only supports a single x axis because the x axis is always a dataset axis - dataset.xAxisID = this.options.scales.xAxes[0].id; - - if (!dataset.yAxisID) { - dataset.yAxisID = this.options.scales.yAxes[0].id; - } - }, this); - - // Build and fit the scale. Needs to happen after the axis IDs have been set - this.buildScale(); - - // Create tooltip instance exclusively for this chart with some defaults. - this.tooltip = new Chart.Tooltip({ - _chart: this.chart, - _data: this.data, - _options: this.options, - }, this); - - // Need to fit scales before we reset elements. - Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); - - // So that we animate from the baseline - this.resetElements(); - - // Update the chart with the latest data. - this.update(); - }, - resetElements: function() { - // Update the points - this.eachElement(function(bar, index, dataset, datasetIndex) { - var xScale = this.scales[this.data.datasets[datasetIndex].xAxisID]; - var yScale = this.scales[this.data.datasets[datasetIndex].yAxisID]; - - var yScalePoint; - - if (yScale.min < 0 && yScale.max < 0) { - // all less than 0. use the top - yScalePoint = yScale.getPixelForValue(yScale.max); - } else if (yScale.min > 0 && yScale.max > 0) { - yScalePoint = yScale.getPixelForValue(yScale.min); - } else { - yScalePoint = yScale.getPixelForValue(0); - } - - helpers.extend(bar, { - // Utility - _chart: this.chart, - _xScale: xScale, - _yScale: yScale, - _datasetIndex: datasetIndex, - _index: index, - - // Desired view properties - _model: { - x: xScale.calculateBarX(this.data.datasets.length, datasetIndex, index), - y: yScalePoint, - - // Appearance - base: yScale.calculateBarBase(datasetIndex, index), - width: xScale.calculateBarWidth(this.data.datasets.length), - backgroundColor: bar.custom && bar.custom.backgroundColor ? bar.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].backgroundColor, index, this.options.elements.rectangle.backgroundColor), - borderColor: bar.custom && bar.custom.borderColor ? bar.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].borderColor, index, this.options.elements.rectangle.borderColor), - borderWidth: bar.custom && bar.custom.borderWidth ? bar.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].borderWidth, index, this.options.elements.rectangle.borderWidth), - - // Tooltip - label: this.data.labels[index], - datasetLabel: this.data.datasets[datasetIndex].label, - }, - }); - bar.pivot(); - }, this); - }, - update: function(animationDuration) { - // Update the scale sizes - Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); - - // Update the points - this.eachElement(function(bar, index, dataset, datasetIndex) { - var xScale = this.scales[this.data.datasets[datasetIndex].xAxisID]; - var yScale = this.scales[this.data.datasets[datasetIndex].yAxisID]; - - helpers.extend(bar, { - // Utility - _chart: this.chart, - _xScale: xScale, - _yScale: yScale, - _datasetIndex: datasetIndex, - _index: index, - - // Desired view properties - _model: { - x: xScale.calculateBarX(this.data.datasets.length, datasetIndex, index), - y: yScale.calculateBarY(datasetIndex, index), - - // Appearance - base: yScale.calculateBarBase(datasetIndex, index), - width: xScale.calculateBarWidth(this.data.datasets.length), - backgroundColor: bar.custom && bar.custom.backgroundColor ? bar.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].backgroundColor, index, this.options.elements.rectangle.backgroundColor), - borderColor: bar.custom && bar.custom.borderColor ? bar.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].borderColor, index, this.options.elements.rectangle.borderColor), - borderWidth: bar.custom && bar.custom.borderWidth ? bar.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].borderWidth, index, this.options.elements.rectangle.borderWidth), - - // Tooltip - label: this.data.labels[index], - datasetLabel: this.data.datasets[datasetIndex].label, - }, - }); - bar.pivot(); - }, this); - - - this.render(animationDuration); - }, - buildScale: function(labels) { - var self = this; - - // Function to determine the range of all the - var calculateYRange = function() { - this.min = null; - this.max = null; - - var positiveValues = []; - var negativeValues = []; - - if (self.options.stacked) { - helpers.each(self.data.datasets, function(dataset) { - if (dataset.yAxisID === this.id) { - helpers.each(dataset.data, function(value, index) { - positiveValues[index] = positiveValues[index] || 0; - negativeValues[index] = negativeValues[index] || 0; - - if (self.options.relativePoints) { - positiveValues[index] = 100; - } else { - if (value < 0) { - negativeValues[index] += value; - } else { - positiveValues[index] += value; - } - } - }, this); - } - }, this); - - var values = positiveValues.concat(negativeValues); - this.min = helpers.min(values); - this.max = helpers.max(values); - - } else { - helpers.each(self.data.datasets, function(dataset) { - if (dataset.yAxisID === this.id) { - helpers.each(dataset.data, function(value, index) { - if (this.min === null) { - this.min = value; - } else if (value < this.min) { - this.min = value; - } - - if (this.max === null) { - this.max = value; - } else if (value > this.max) { - this.max = value; - } - }, this); - } - }, this); - } - }; - - // Map of scale ID to scale object so we can lookup later - this.scales = {}; - - // Build the x axis. The line chart only supports a single x axis - var ScaleClass = Chart.scales.getScaleConstructor(this.options.scales.xAxes[0].type); - var xScale = new ScaleClass({ - ctx: this.chart.ctx, - options: this.options.scales.xAxes[0], - id: this.options.scales.xAxes[0].id, - calculateRange: function() { - this.labels = self.data.labels; - this.min = 0; - this.max = this.labels.length; - }, - calculateBaseWidth: function() { - return (this.getPixelForValue(null, 1, true) - this.getPixelForValue(null, 0, true)) - (2 * self.options.valueSpacing); - }, - 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) * self.options.datasetSpacing); - - if (self.options.stacked) { - return baseWidth; - } - return (baseWidth / datasetCount); - }, - calculateBarX: function(datasetCount, datasetIndex, elementIndex) { - var xWidth = this.calculateBaseWidth(), - xAbsolute = this.getPixelForValue(null, elementIndex, true) - (xWidth / 2), - barWidth = this.calculateBarWidth(datasetCount); - - if (self.options.stacked) { - return xAbsolute + barWidth / 2; - } - - return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * self.options.datasetSpacing) + barWidth / 2; - }, - }); - this.scales[xScale.id] = xScale; - - // Build up all the y scales - helpers.each(this.options.scales.yAxes, function(yAxisOptions) { - var ScaleClass = Chart.scales.getScaleConstructor(yAxisOptions.type); - var scale = new ScaleClass({ - ctx: this.chart.ctx, - options: yAxisOptions, - calculateRange: calculateYRange, - calculateBarBase: function(datasetIndex, index) { - var base = 0; - - if (self.options.stacked) { - - var value = self.data.datasets[datasetIndex].data[index]; - - if (value < 0) { - for (var i = 0; i < datasetIndex; i++) { - if (self.data.datasets[i].yAxisID === this.id) { - base += self.data.datasets[i].data[index] < 0 ? self.data.datasets[i].data[index] : 0; - } - } - } else { - for (var j = 0; j < datasetIndex; j++) { - if (self.data.datasets[j].yAxisID === this.id) { - base += self.data.datasets[j].data[index] > 0 ? self.data.datasets[j].data[index] : 0; - } - } - } - - return this.getPixelForValue(base); - } - - base = this.getPixelForValue(this.min); - - if (this.beginAtZero || ((this.min <= 0 && this.max >= 0) || (this.min >= 0 && this.max <= 0))) { - base = this.getPixelForValue(0); - base += this.options.gridLines.lineWidth; - } else if (this.min < 0 && this.max < 0) { - // All values are negative. Use the top as the base - base = this.getPixelForValue(this.max); - } - - return base; - - }, - calculateBarY: function(datasetIndex, index) { - - var value = self.data.datasets[datasetIndex].data[index]; - - if (self.options.stacked) { - - var sumPos = 0, - sumNeg = 0; - - for (var i = 0; i < datasetIndex; i++) { - if (self.data.datasets[i].data[index] < 0) { - sumNeg += self.data.datasets[i].data[index] || 0; - } else { - sumPos += self.data.datasets[i].data[index] || 0; - } - } - - if (value < 0) { - return this.getPixelForValue(sumNeg + value); - } else { - return this.getPixelForValue(sumPos + value); - } - - return this.getPixelForValue(value); - } - - var offset = 0; - - for (var j = datasetIndex; j < self.data.datasets.length; j++) { - if (j === datasetIndex && value) { - offset += value; - } else { - offset = offset + value; - } - } - - return this.getPixelForValue(value); - }, - id: yAxisOptions.id, - }); - - this.scales[scale.id] = scale; - }, this); - }, - draw: function(ease) { - - var easingDecimal = ease || 1; - this.clear(); - - // Draw all the scales - helpers.each(this.scales, function(scale) { - scale.draw(this.chartArea); - }, this); - - //Draw all the bars for each dataset - this.eachElement(function(bar, index, datasetIndex) { - bar.transition(easingDecimal).draw(); - }, this); - - // Finally draw the tooltip - this.tooltip.transition(easingDecimal).draw(); - }, - events: function(e) { - - - // If exiting chart - if (e.type == 'mouseout') { - return this; - } - - this.lastActive = this.lastActive || []; - - // Find Active Elements - this.active = function() { - switch (this.options.hover.mode) { - case 'single': - return this.getElementAtEvent(e); - case 'label': - return this.getElementsAtEvent(e); - case 'dataset': - return this.getDatasetAtEvent(e); - default: - return e; - } - }.call(this); - - // On Hover hook - if (this.options.hover.onHover) { - this.options.hover.onHover.call(this, this.active); - } - - if (e.type == 'mouseup' || e.type == 'click') { - if (this.options.onClick) { - this.options.onClick.call(this, e, this.active); - } - } - - var dataset; - var index; - // Remove styling for last active (even if it may still be active) - if (this.lastActive.length) { - switch (this.options.hover.mode) { - case 'single': - dataset = this.data.datasets[this.lastActive[0]._datasetIndex]; - index = this.lastActive[0]._index; - - this.lastActive[0]._model.backgroundColor = this.lastActive[0].custom && this.lastActive[0].custom.backgroundColor ? this.lastActive[0].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.rectangle.backgroundColor); - this.lastActive[0]._model.borderColor = this.lastActive[0].custom && this.lastActive[0].custom.borderColor ? this.lastActive[0].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.rectangle.borderColor); - this.lastActive[0]._model.borderWidth = this.lastActive[0].custom && this.lastActive[0].custom.borderWidth ? this.lastActive[0].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.rectangle.borderWidth); - break; - case 'label': - for (var i = 0; i < this.lastActive.length; i++) { - dataset = this.data.datasets[this.lastActive[i]._datasetIndex]; - index = this.lastActive[i]._index; - - this.lastActive[i]._model.backgroundColor = this.lastActive[i].custom && this.lastActive[i].custom.backgroundColor ? this.lastActive[i].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.rectangle.backgroundColor); - this.lastActive[i]._model.borderColor = this.lastActive[i].custom && this.lastActive[i].custom.borderColor ? this.lastActive[i].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.rectangle.borderColor); - this.lastActive[i]._model.borderWidth = this.lastActive[i].custom && this.lastActive[i].custom.borderWidth ? this.lastActive[i].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.rectangle.borderWidth); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - // Built in hover styling - if (this.active.length && this.options.hover.mode) { - switch (this.options.hover.mode) { - case 'single': - dataset = this.data.datasets[this.active[0]._datasetIndex]; - index = this.active[0]._index; - - this.active[0]._model.backgroundColor = this.active[0].custom && this.active[0].custom.hoverBackgroundColor ? this.active[0].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(this.active[0]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - this.active[0]._model.borderColor = this.active[0].custom && this.active[0].custom.hoverBorderColor ? this.active[0].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(this.active[0]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - this.active[0]._model.borderWidth = this.active[0].custom && this.active[0].custom.hoverBorderWidth ? this.active[0].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.active[0]._model.borderWidth); - break; - case 'label': - for (var i = 0; i < this.active.length; i++) { - dataset = this.data.datasets[this.active[i]._datasetIndex]; - index = this.active[i]._index; - - this.active[i]._model.backgroundColor = this.active[i].custom && this.active[i].custom.hoverBackgroundColor ? this.active[i].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(this.active[i]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - this.active[i]._model.borderColor = this.active[i].custom && this.active[i].custom.hoverBorderColor ? this.active[i].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(this.active[i]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - this.active[i]._model.borderWidth = this.active[i].custom && this.active[i].custom.hoverBorderWidth ? this.active[i].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.active[i]._model.borderWidth); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - - // Built in Tooltips - if (this.options.tooltips.enabled) { - - // The usual updates - this.tooltip.initialize(); - - // Active - if (this.active.length) { - this.tooltip._model.opacity = 1; - - helpers.extend(this.tooltip, { - _active: this.active, - }); - - this.tooltip.update(); - } else { - // Inactive - this.tooltip._model.opacity = 0; - } - } - - - this.tooltip.pivot(); - - // Hover animations - if (!this.animating) { - var changed; - - helpers.each(this.active, function(element, index) { - if (element !== this.lastActive[index]) { - changed = true; - } - }, this); - - // If entering, leaving, or changing elements, animate the change via pivot - if ((!this.lastActive.length && this.active.length) || - (this.lastActive.length && !this.active.length) || - (this.lastActive.length && this.active.length && changed)) { - - this.stop(); - this.render(this.options.hoverAnimationDuration); - } - } - - // Remember Last Active - this.lastActive = this.active; - return this; - }, - }); + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + Chart.defaults.global.elements.point = { + radius: 3, + backgroundColor: Chart.defaults.global.defaultColor, + borderWidth: 1, + borderColor: Chart.defaults.global.defaultColor, + // Hover + hitRadius: 1, + hoverRadius: 4, + hoverBorderWidth: 1, + }; + + + Chart.elements.Point = Chart.Element.extend({ + inRange: function(mouseX, mouseY) { + var vm = this._view; + var hoverRange = vm.hitRadius + vm.radius; + return ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(hoverRange, 2)); + }, + inGroupRange: function(mouseX) { + var vm = this._view; + + if (vm) { + return (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)); + } else { + return false; + } + }, + tooltipPosition: function() { + var vm = this._view; + return { + x: vm.x, + y: vm.y, + padding: vm.radius + vm.borderWidth + }; + }, + draw: function() { + + var vm = this._view; + var ctx = this._chart.ctx; + + + if (vm.skip) { + return; + } + + if (vm.radius > 0 || vm.borderWidth > 0) { + + ctx.beginPath(); + + ctx.arc(vm.x, vm.y, vm.radius || Chart.defaults.global.elements.point.radius, 0, Math.PI * 2); + ctx.closePath(); + + ctx.strokeStyle = vm.borderColor || Chart.defaults.global.defaultColor; + ctx.lineWidth = vm.borderWidth || Chart.defaults.global.elements.point.borderWidth; + + ctx.fillStyle = vm.backgroundColor || Chart.defaults.global.defaultColor; + + ctx.fill(); + ctx.stroke(); + } + } + }); }).call(this); (function() { - "use strict"; - var root = this, - Chart = root.Chart, - //Cache a local reference to Chart.helpers - helpers = Chart.helpers; + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + Chart.defaults.global.elements.rectangle = { + backgroundColor: Chart.defaults.global.defaultColor, + borderWidth: 0, + borderColor: Chart.defaults.global.defaultColor, + }; + + Chart.elements.Rectangle = Chart.Element.extend({ + draw: function() { + + var ctx = this._chart.ctx; + var vm = this._view; + + var halfWidth = vm.width / 2, + leftX = vm.x - halfWidth, + rightX = vm.x + halfWidth, + top = vm.base - (vm.base - vm.y), + halfStroke = vm.borderWidth / 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 (vm.borderWidth) { + leftX += halfStroke; + rightX -= halfStroke; + top += halfStroke; + } + + ctx.beginPath(); + + ctx.fillStyle = vm.backgroundColor; + ctx.strokeStyle = vm.borderColor; + ctx.lineWidth = vm.borderWidth; + + // It'd be nice to keep this class totally generic to any rectangle + // and simply specify which border to miss out. + ctx.moveTo(leftX, vm.base); + ctx.lineTo(leftX, top); + ctx.lineTo(rightX, top); + ctx.lineTo(rightX, vm.base); + ctx.fill(); + if (vm.borderWidth) { + ctx.stroke(); + } + }, + height: function() { + var vm = this._view; + return vm.base - vm.y; + }, + inRange: function(mouseX, mouseY) { + var vm = this._view; + if (vm.y < vm.base) { + return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.y && mouseY <= vm.base); + } else { + return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.base && mouseY <= vm.y); + } + }, + inGroupRange: function(mouseX) { + var vm = this._view; + return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2); + }, + tooltipPosition: function() { + var vm = this._view; + if (vm.y < vm.base) { + return { + x: vm.x, + y: vm.y + }; + } else { + return { + x: vm.x, + y: vm.base + }; + } + }, + }); - var defaultConfig = { - - animation: { - //Boolean - Whether we animate the rotation of the Doughnut - animateRotate: true, - - //Boolean - Whether we animate scaling the Doughnut from the centre - animateScale: false, - }, - - hover: { - mode: 'single' - }, - - //The percentage of the chart that we cut out of the middle. - - cutoutPercentage: 50, - - }; - - 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() { - - //Set up tooltip events on the chart - helpers.bindEvents(this, this.options.events, this.events); - - //Create a new bar for each piece of data - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - dataset.metaData = []; - helpers.each(dataset.data, function(dataPoint, index) { - dataset.metaData.push(new Chart.Arc({ - _chart: this.chart, - _datasetIndex: datasetIndex, - _index: index, - _model: {} - })); - }, this); - }, this); - - // Create tooltip instance exclusively for this chart with some defaults. - this.tooltip = new Chart.Tooltip({ - _chart: this.chart, - _data: this.data, - _options: this.options, - }, this); - - this.resetElements(); - - // Update the chart with the latest data. - this.update(); - - }, - - calculateCircumference: function(dataset, value) { - if (dataset.total > 0) { - return (Math.PI * 2) * (value / dataset.total); - } else { - return 0; - } - }, - resetElements: function() { - this.outerRadius = (helpers.min([this.chart.width, this.chart.height]) - this.options.elements.arc.borderWidth / 2) / 2; - this.innerRadius = this.options.cutoutPercentage ? (this.outerRadius / 100) * (this.options.cutoutPercentage) : 1; - this.radiusLength = (this.outerRadius - this.innerRadius) / this.data.datasets.length; - - // Update the points - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - // So that calculateCircumference works - dataset.total = 0; - helpers.each(dataset.data, function(value) { - dataset.total += Math.abs(value); - }, this); - - dataset.outerRadius = this.outerRadius - (this.radiusLength * datasetIndex); - dataset.innerRadius = dataset.outerRadius - this.radiusLength; - - helpers.each(dataset.metaData, function(slice, index) { - helpers.extend(slice, { - _model: { - x: this.chart.width / 2, - y: this.chart.height / 2, - startAngle: Math.PI * -0.5, // use - PI / 2 instead of 3PI / 2 to make animations better. It means that we never deal with overflow during the transition function - circumference: (this.options.animation.animateRotate) ? 0 : this.calculateCircumference(metaSlice.value), - outerRadius: (this.options.animation.animateScale) ? 0 : dataset.outerRadius, - innerRadius: (this.options.animation.animateScale) ? 0 : dataset.innerRadius, - - backgroundColor: slice.custom && slice.custom.backgroundColor ? slice.custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.arc.backgroundColor), - hoverBackgroundColor: slice.custom && slice.custom.hoverBackgroundColor ? slice.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, this.options.elements.arc.hoverBackgroundColor), - borderWidth: slice.custom && slice.custom.borderWidth ? slice.custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.arc.borderWidth), - borderColor: slice.custom && slice.custom.borderColor ? slice.custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.arc.borderColor), - - label: helpers.getValueAtIndexOrDefault(dataset.label, index, this.data.labels[index]) - }, - }); - - slice.pivot(); - }, this); - - }, this); - }, - update: function(animationDuration) { - - this.outerRadius = (helpers.min([this.chart.width, this.chart.height]) - this.options.elements.arc.borderWidth / 2) / 2; - this.innerRadius = this.options.cutoutPercentage ? (this.outerRadius / 100) * (this.options.cutoutPercentage) : 1; - this.radiusLength = (this.outerRadius - this.innerRadius) / this.data.datasets.length; - - - // Update the points - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - - dataset.total = 0; - helpers.each(dataset.data, function(value) { - dataset.total += Math.abs(value); - }, this); - - - dataset.outerRadius = this.outerRadius - (this.radiusLength * datasetIndex); - - dataset.innerRadius = dataset.outerRadius - this.radiusLength; - - helpers.each(dataset.metaData, function(slice, index) { - - helpers.extend(slice, { - // Utility - _chart: this.chart, - _datasetIndex: datasetIndex, - _index: index, - - // Desired view properties - _model: { - x: this.chart.width / 2, - y: this.chart.height / 2, - circumference: this.calculateCircumference(dataset, dataset.data[index]), - outerRadius: dataset.outerRadius, - innerRadius: dataset.innerRadius, - - backgroundColor: slice.custom && slice.custom.backgroundColor ? slice.custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.arc.backgroundColor), - hoverBackgroundColor: slice.custom && slice.custom.hoverBackgroundColor ? slice.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, this.options.elements.arc.hoverBackgroundColor), - borderWidth: slice.custom && slice.custom.borderWidth ? slice.custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.arc.borderWidth), - borderColor: slice.custom && slice.custom.borderColor ? slice.custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.arc.borderColor), - - label: helpers.getValueAtIndexOrDefault(dataset.label, index, this.data.labels[index]) - }, - }); - - if (index === 0) { - slice._model.startAngle = Math.PI * -0.5; // use - PI / 2 instead of 3PI / 2 to make animations better. It means that we never deal with overflow during the transition function - } else { - slice._model.startAngle = dataset.metaData[index - 1]._model.endAngle; - } - - slice._model.endAngle = slice._model.startAngle + slice._model.circumference; - - - //Check to see if it's the last slice, if not get the next and update its start angle - if (index < dataset.data.length - 1) { - dataset.metaData[index + 1]._model.startAngle = slice._model.endAngle; - } - - slice.pivot(); - }, this); - - }, this); - - this.render(animationDuration); - }, - draw: function(easeDecimal) { - easeDecimal = easeDecimal || 1; - this.clear(); - - this.eachElement(function(slice) { - slice.transition(easeDecimal).draw(); - }, this); - - this.tooltip.transition(easeDecimal).draw(); - }, - events: function(e) { - - this.lastActive = this.lastActive || []; - - // Find Active Elements - if (e.type == 'mouseout') { - this.active = []; - } else { - - this.active = function() { - switch (this.options.hover.mode) { - case 'single': - return this.getSliceAtEvent(e); - case 'label': - return this.getSlicesAtEvent(e); - case 'dataset': - return this.getDatasetAtEvent(e); - default: - return e; - } - }.call(this); - } - - // On Hover hook - if (this.options.hover.onHover) { - this.options.hover.onHover.call(this, this.active); - } - - if (e.type == 'mouseup' || e.type == 'click') { - if (this.options.onClick) { - this.options.onClick.call(this, e, this.active); - } - } - - var dataset; - var index; - // Remove styling for last active (even if it may still be active) - if (this.lastActive.length) { - switch (this.options.hover.mode) { - case 'single': - dataset = this.data.datasets[this.lastActive[0]._datasetIndex]; - index = this.lastActive[0]._index; - - this.lastActive[0]._model.backgroundColor = this.lastActive[0].custom && this.lastActive[0].custom.backgroundColor ? this.lastActive[0].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.arc.backgroundColor); - this.lastActive[0]._model.borderColor = this.lastActive[0].custom && this.lastActive[0].custom.borderColor ? this.lastActive[0].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.arc.borderColor); - this.lastActive[0]._model.borderWidth = this.lastActive[0].custom && this.lastActive[0].custom.borderWidth ? this.lastActive[0].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.arc.borderWidth); - break; - case 'label': - for (var i = 0; i < this.lastActive.length; i++) { - dataset = this.data.datasets[this.lastActive[i]._datasetIndex]; - index = this.lastActive[i]._index; - - this.lastActive[i]._model.backgroundColor = this.lastActive[i].custom && this.lastActive[i].custom.backgroundColor ? this.lastActive[i].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.arc.backgroundColor); - this.lastActive[i]._model.borderColor = this.lastActive[i].custom && this.lastActive[i].custom.borderColor ? this.lastActive[i].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.arc.borderColor); - this.lastActive[i]._model.borderWidth = this.lastActive[i].custom && this.lastActive[i].custom.borderWidth ? this.lastActive[i].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.arc.borderWidth); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - // Built in hover styling - if (this.active.length && this.options.hover.mode) { - switch (this.options.hover.mode) { - case 'single': - dataset = this.data.datasets[this.active[0]._datasetIndex]; - index = this.active[0]._index; - - this.active[0]._model.backgroundColor = this.active[0].custom && this.active[0].custom.hoverBackgroundColor ? this.active[0].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(this.active[0]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - this.active[0]._model.borderColor = this.active[0].custom && this.active[0].custom.hoverBorderColor ? this.active[0].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, this.active[0]._model.borderColor); - this.active[0]._model.borderWidth = this.active[0].custom && this.active[0].custom.hoverBorderWidth ? this.active[0].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, this.active[0]._model.borderWidth); - break; - case 'label': - for (var i = 0; i < this.active.length; i++) { - dataset = this.data.datasets[this.active[i]._datasetIndex]; - index = this.active[i]._index; - - this.active[i]._model.backgroundColor = this.active[i].custom && this.active[i].custom.hoverBackgroundColor ? this.active[i].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(this.active[i]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - this.active[i]._model.borderColor = this.active[i].custom && this.active[i].custom.hoverBorderColor ? this.active[i].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, this.active[0]._model.borderColor); - this.active[i]._model.borderWidth = this.active[i].custom && this.active[i].custom.hoverBorderWidth ? this.active[i].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, this.active[i]._model.borderWidth); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - - // Built in Tooltips - if (this.options.tooltips.enabled) { - - // The usual updates - this.tooltip.initialize(); - - // Active - if (this.active.length) { - this.tooltip._model.opacity = 1; - - helpers.extend(this.tooltip, { - _active: this.active, - }); - - this.tooltip.update(); - } else { - // Inactive - this.tooltip._model.opacity = 0; - } - } - - - // Hover animations - this.tooltip.pivot(); - - if (!this.animating) { - var changed; - - helpers.each(this.active, function(element, index) { - if (element !== this.lastActive[index]) { - changed = true; - } - }, this); - - // If entering, leaving, or changing elements, animate the change via pivot - if ((!this.lastActive.length && this.active.length) || - (this.lastActive.length && !this.active.length) || - (this.lastActive.length && this.active.length && changed)) { - - this.stop(); - this.render(this.options.hover.animationDuration); - } - } - - // Remember Last Active - this.lastActive = this.active; - return this; - }, - getSliceAtEvent: function(e) { - var elements = []; - - var location = helpers.getRelativePosition(e); - - this.eachElement(function(slice, index) { - if (slice.inRange(location.x, location.y)) { - elements.push(slice); - } - }, this); - return elements; - }, - /*getSlicesAtEvent: function(e) { - var elements = []; - - var location = helpers.getRelativePosition(e); - - this.eachElement(function(slice, index) { - if (slice.inGroupRange(location.x, location.y)) { - elements.push(slice); - } - }, this); - return elements; - },*/ - }); - - Chart.types.Doughnut.extend({ - name: "Pie", - defaults: helpers.merge(defaultConfig, { - cutoutPercentage: 0 - }) - }); - -}).call(this); - -(function() { - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - var defaultConfig = { - - stacked: false, - - hover: { - mode: "label" - }, - - scales: { - xAxes: [{ - type: "category", // scatter should not use a dataset axis - display: true, - position: "bottom", - id: "x-axis-1", // need an ID so datasets can reference the scale - - // grid line settings - gridLines: { - show: true, - color: "rgba(0, 0, 0, 0.05)", - lineWidth: 1, - drawOnChartArea: true, - drawTicks: true, - zeroLineWidth: 1, - zeroLineColor: "rgba(0,0,0,0.25)", - offsetGridLines: false, - }, - - // label settings - labels: { - show: true, - template: "<%=value%>", - fontSize: 12, - fontStyle: "normal", - fontColor: "#666", - fontFamily: "Helvetica Neue", - }, - }], - yAxes: [{ - type: "linear", // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance - display: true, - position: "left", - id: "y-axis-1", - - // grid line settings - gridLines: { - show: true, - color: "rgba(0, 0, 0, 0.05)", - lineWidth: 1, - drawOnChartArea: true, - drawTicks: true, // draw ticks extending towards the label - zeroLineWidth: 1, - zeroLineColor: "rgba(0,0,0,0.25)", - }, - - // scale numbers - beginAtZero: false, - override: null, - - // label settings - labels: { - show: true, - template: "<%=value.toLocaleString()%>", - fontSize: 12, - fontStyle: "normal", - fontColor: "#666", - fontFamily: "Helvetica Neue", - } - }], - }, - }; - - - Chart.Type.extend({ - name: "Line", - defaults: defaultConfig, - initialize: function() { - - var _this = this; - - // Events - helpers.bindEvents(this, this.options.events, this.events); - - // Create a new line and its points for each dataset and piece of data - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - - dataset.metaDataset = new Chart.Line({ - _chart: this.chart, - _datasetIndex: datasetIndex, - _points: dataset.metaData, - }); - - dataset.metaData = []; - - helpers.each(dataset.data, function(dataPoint, index) { - dataset.metaData.push(new Chart.Point({ - _datasetIndex: datasetIndex, - _index: index, - _chart: this.chart, - _model: { - x: 0, //xScale.getPixelForValue(null, index, true), - y: 0, //this.chartArea.bottom, - }, - })); - - }, this); - - // The line chart onlty supports a single x axis because the x axis is always a dataset axis - dataset.xAxisID = this.options.scales.xAxes[0].id; - - if (!dataset.yAxisID) { - dataset.yAxisID = this.options.scales.yAxes[0].id; - } - - }, this); - - // Build and fit the scale. Needs to happen after the axis IDs have been set - this.buildScale(); - - // Create tooltip instance exclusively for this chart with some defaults. - this.tooltip = new Chart.Tooltip({ - _chart: this.chart, - _data: this.data, - _options: this.options, - }, this); - - // Need to fit scales before we reset elements. - Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); - - // Reset so that we animation from the baseline - this.resetElements(); - - // Update that shiz - this.update(); - }, - nextPoint: function(collection, index) { - return collection[index + 1] || collection[index]; - }, - previousPoint: function(collection, index) { - return collection[index - 1] || collection[index]; - }, - resetElements: function() { - // Update the points - this.eachElement(function(point, index, dataset, datasetIndex) { - var xScale = this.scales[this.data.datasets[datasetIndex].xAxisID]; - var yScale = this.scales[this.data.datasets[datasetIndex].yAxisID]; - - var yScalePoint; - - if (yScale.min < 0 && yScale.max < 0) { - // all less than 0. use the top - yScalePoint = yScale.getPixelForValue(yScale.max); - } else if (yScale.min > 0 && yScale.max > 0) { - yScalePoint = yScale.getPixelForValue(yScale.min); - } else { - yScalePoint = yScale.getPixelForValue(0); - } - - helpers.extend(point, { - // Utility - _chart: this.chart, - _xScale: xScale, - _yScale: yScale, - _datasetIndex: datasetIndex, - _index: index, - - // Desired view properties - _model: { - x: xScale.getPixelForValue(null, index, true), // value not used in dataset scale, but we want a consistent API between scales - y: yScalePoint, - - // Appearance - tension: point.custom && point.custom.tension ? point.custom.tension : this.options.elements.line.tension, - radius: point.custom && point.custom.radius ? point.custom.radius : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].radius, index, this.options.elements.point.radius), - backgroundColor: point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBackgroundColor, index, this.options.elements.point.backgroundColor), - borderColor: point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBorderColor, index, this.options.elements.point.borderColor), - borderWidth: point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBorderWidth, index, this.options.elements.point.borderWidth), - skip: this.data.datasets[datasetIndex].data[index] === null, - - // Tooltip - hitRadius: point.custom && point.custom.hitRadius ? point.custom.hitRadius : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].hitRadius, index, this.options.elements.point.hitRadius), - }, - }); - }, this); - - // Update control points for the bezier curve - this.eachElement(function(point, index, dataset, datasetIndex) { - var controlPoints = helpers.splineCurve( - this.previousPoint(dataset, index)._model, - point._model, - this.nextPoint(dataset, index)._model, - point._model.tension - ); - - point._model.controlPointPreviousX = controlPoints.previous.x; - point._model.controlPointNextX = controlPoints.next.x; - - // Prevent the bezier going outside of the bounds of the graph - - // Cap puter bezier handles to the upper/lower scale bounds - if (controlPoints.next.y > this.chartArea.bottom) { - point._model.controlPointNextY = this.chartArea.bottom; - } else if (controlPoints.next.y < this.chartArea.top) { - point._model.controlPointNextY = this.chartArea.top; - } else { - point._model.controlPointNextY = controlPoints.next.y; - } - - // Cap inner bezier handles to the upper/lower scale bounds - if (controlPoints.previous.y > this.chartArea.bottom) { - point._model.controlPointPreviousY = this.chartArea.bottom; - } else if (controlPoints.previous.y < this.chartArea.top) { - point._model.controlPointPreviousY = this.chartArea.top; - } else { - point._model.controlPointPreviousY = controlPoints.previous.y; - } - - // Now pivot the point for animation - point.pivot(); - }, this); - }, - update: function(animationDuration) { - - Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); - - // Update the lines - this.eachDataset(function(dataset, datasetIndex) { - var yScale = this.scales[dataset.yAxisID]; - var scaleBase; - - if (yScale.min < 0 && yScale.max < 0) { - scaleBase = yScale.getPixelForValue(yScale.max); - } else if (yScale.min > 0 && yScale.max > 0) { - scaleBase = yScale.getPixelForValue(yScale.min); - } else { - scaleBase = yScale.getPixelForValue(0); - } - - helpers.extend(dataset.metaDataset, { - // Utility - _scale: yScale, - _datasetIndex: datasetIndex, - // Data - _children: dataset.metaData, - // Model - _model: { - // Appearance - tension: dataset.metaDataset.custom && dataset.metaDataset.custom.tension ? dataset.metaDataset.custom.tension : (dataset.tension || this.options.elements.line.tension), - backgroundColor: dataset.metaDataset.custom && dataset.metaDataset.custom.backgroundColor ? dataset.metaDataset.custom.backgroundColor : (dataset.backgroundColor || this.options.elements.line.backgroundColor), - borderWidth: dataset.metaDataset.custom && dataset.metaDataset.custom.borderWidth ? dataset.metaDataset.custom.borderWidth : (dataset.borderWidth || this.options.elements.line.borderWidth), - borderColor: dataset.metaDataset.custom && dataset.metaDataset.custom.borderColor ? dataset.metaDataset.custom.borderColor : (dataset.borderColor || this.options.elements.line.borderColor), - fill: dataset.metaDataset.custom && dataset.metaDataset.custom.fill ? dataset.metaDataset.custom.fill : (dataset.fill !== undefined ? dataset.fill : this.options.elements.line.fill), - skipNull: dataset.skipNull !== undefined ? dataset.skipNull : this.options.elements.line.skipNull, - drawNull: dataset.drawNull !== undefined ? dataset.drawNull : this.options.elements.line.drawNull, - // Scale - scaleTop: yScale.top, - scaleBottom: yScale.bottom, - scaleZero: scaleBase, - }, - }); - - dataset.metaDataset.pivot(); - }); - - // Update the points - this.eachElement(function(point, index, dataset, datasetIndex) { - var xScale = this.scales[this.data.datasets[datasetIndex].xAxisID]; - var yScale = this.scales[this.data.datasets[datasetIndex].yAxisID]; - - helpers.extend(point, { - // Utility - _chart: this.chart, - _xScale: xScale, - _yScale: yScale, - _datasetIndex: datasetIndex, - _index: index, - - // Desired view properties - _model: { - x: xScale.getPixelForValue(null, index, true), // value not used in dataset scale, but we want a consistent API between scales - y: yScale.getPointPixelForValue(this.data.datasets[datasetIndex].data[index], index, datasetIndex), - - // Appearance - tension: point.custom && point.custom.tension ? point.custom.tension : this.options.elements.line.tension, - radius: point.custom && point.custom.radius ? point.custom.radius : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].radius, index, this.options.elements.point.radius), - backgroundColor: point.custom && point.custom.backgroundColor ? point.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBackgroundColor, index, this.options.elements.point.backgroundColor), - borderColor: point.custom && point.custom.borderColor ? point.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBorderColor, index, this.options.elements.point.borderColor), - borderWidth: point.custom && point.custom.borderWidth ? point.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].pointBorderWidth, index, this.options.elements.point.borderWidth), - skip: this.data.datasets[datasetIndex].data[index] === null, - - // Tooltip - hitRadius: point.custom && point.custom.hitRadius ? point.custom.hitRadius : helpers.getValueAtIndexOrDefault(this.data.datasets[datasetIndex].hitRadius, index, this.options.elements.point.hitRadius), - }, - }); - }, this); - - - // Update control points for the bezier curve - this.eachElement(function(point, index, dataset, datasetIndex) { - var controlPoints = helpers.splineCurve( - this.previousPoint(dataset, index)._model, - point._model, - this.nextPoint(dataset, index)._model, - point._model.tension - ); - - point._model.controlPointPreviousX = controlPoints.previous.x; - point._model.controlPointNextX = controlPoints.next.x; - - // Prevent the bezier going outside of the bounds of the graph - - // Cap puter bezier handles to the upper/lower scale bounds - if (controlPoints.next.y > this.chartArea.bottom) { - point._model.controlPointNextY = this.chartArea.bottom; - } else if (controlPoints.next.y < this.chartArea.top) { - point._model.controlPointNextY = this.chartArea.top; - } else { - point._model.controlPointNextY = controlPoints.next.y; - } - - // Cap inner bezier handles to the upper/lower scale bounds - if (controlPoints.previous.y > this.chartArea.bottom) { - point._model.controlPointPreviousY = this.chartArea.bottom; - } else if (controlPoints.previous.y < this.chartArea.top) { - point._model.controlPointPreviousY = this.chartArea.top; - } else { - point._model.controlPointPreviousY = controlPoints.previous.y; - } - - // Now pivot the point for animation - point.pivot(); - }, this); - - this.render(animationDuration); - }, - buildScale: function() { - var self = this; - - // Function to determine the range of all the - var calculateYRange = function() { - this.min = null; - this.max = null; - - var positiveValues = []; - var negativeValues = []; - - if (self.options.stacked) { - helpers.each(self.data.datasets, function(dataset) { - if (dataset.yAxisID === this.id) { - helpers.each(dataset.data, function(value, index) { - positiveValues[index] = positiveValues[index] || 0; - negativeValues[index] = negativeValues[index] || 0; - - if (self.options.relativePoints) { - positiveValues[index] = 100; - } else { - if (value < 0) { - negativeValues[index] += value; - } else { - positiveValues[index] += value; - } - } - }, this); - } - }, this); - - var values = positiveValues.concat(negativeValues); - this.min = helpers.min(values); - this.max = helpers.max(values); - } else { - helpers.each(self.data.datasets, function(dataset) { - if (dataset.yAxisID === this.id) { - helpers.each(dataset.data, function(value, index) { - if (this.min === null) { - this.min = value; - } else if (value < this.min) { - this.min = value; - } - - if (this.max === null) { - this.max = value; - } else if (value > this.max) { - this.max = value; - } - }, this); - } - }, this); - } - }; - - // Map of scale ID to scale object so we can lookup later - this.scales = {}; - - // Build the x axis. The line chart only supports a single x axis - var ScaleClass = Chart.scales.getScaleConstructor(this.options.scales.xAxes[0].type); - var xScale = new ScaleClass({ - ctx: this.chart.ctx, - options: this.options.scales.xAxes[0], - calculateRange: function() { - this.labels = self.data.labels; - this.min = 0; - this.max = this.labels.length; - }, - id: this.options.scales.xAxes[0].id, - }); - this.scales[xScale.id] = xScale; - - // Build up all the y scales - helpers.each(this.options.scales.yAxes, function(yAxisOptions) { - var ScaleClass = Chart.scales.getScaleConstructor(yAxisOptions.type); - var scale = new ScaleClass({ - ctx: this.chart.ctx, - options: yAxisOptions, - calculateRange: calculateYRange, - getPointPixelForValue: function(value, index, datasetIndex) { - if (self.options.stacked) { - var offsetPos = 0; - var offsetNeg = 0; - - for (var i = 0; i < datasetIndex; ++i) { - if (self.data.datasets[i].data[index] < 0) { - offsetNeg += self.data.datasets[i].data[index]; - } else { - offsetPos += self.data.datasets[i].data[index]; - } - } - - if (value < 0) { - return this.getPixelForValue(offsetNeg + value); - } else { - return this.getPixelForValue(offsetPos + value); - } - } else { - return this.getPixelForValue(value); - } - }, - id: yAxisOptions.id, - }); - - this.scales[scale.id] = scale; - }, this); - }, - draw: function(ease) { - - var easingDecimal = ease || 1; - this.clear(); - - // Draw all the scales - helpers.each(this.scales, function(scale) { - scale.draw(this.chartArea); - }, this); - - // reverse for-loop for proper stacking - for (var i = this.data.datasets.length - 1; i >= 0; i--) { - - var dataset = this.data.datasets[i]; - - // Transition Point Locations - helpers.each(dataset.metaData, function(point, index) { - point.transition(easingDecimal); - }, this); - - // Transition and Draw the line - dataset.metaDataset.transition(easingDecimal).draw(); - - // Draw the points - helpers.each(dataset.metaData, function(point) { - point.draw(); - }); - } - - // Finally draw the tooltip - this.tooltip.transition(easingDecimal).draw(); - }, - events: function(e) { - - this.lastActive = this.lastActive || []; - - // Find Active Elements - if (e.type == 'mouseout') { - this.active = []; - } else { - this.active = function() { - switch (this.options.hover.mode) { - case 'single': - return this.getElementAtEvent(e); - case 'label': - return this.getElementsAtEvent(e); - case 'dataset': - return this.getDatasetAtEvent(e); - default: - return e; - } - }.call(this); - } - - // On Hover hook - if (this.options.hover.onHover) { - this.options.hover.onHover.call(this, this.active); - } - - if (e.type == 'mouseup' || e.type == 'click') { - if (this.options.onClick) { - this.options.onClick.call(this, e, this.active); - } - } - - var dataset; - var index; - // Remove styling for last active (even if it may still be active) - if (this.lastActive.length) { - switch (this.options.hover.mode) { - case 'single': - dataset = this.data.datasets[this.lastActive[0]._datasetIndex]; - index = this.lastActive[0]._index; - - this.lastActive[0]._model.radius = this.lastActive[0].custom && this.lastActive[0].custom.radius ? this.lastActive[0].custom.radius : helpers.getValueAtIndexOrDefault(dataset.radius, index, this.options.elements.point.radius); - this.lastActive[0]._model.backgroundColor = this.lastActive[0].custom && this.lastActive[0].custom.backgroundColor ? this.lastActive[0].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointBackgroundColor, index, this.options.elements.point.backgroundColor); - this.lastActive[0]._model.borderColor = this.lastActive[0].custom && this.lastActive[0].custom.borderColor ? this.lastActive[0].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.pointBorderColor, index, this.options.elements.point.borderColor); - this.lastActive[0]._model.borderWidth = this.lastActive[0].custom && this.lastActive[0].custom.borderWidth ? this.lastActive[0].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.options.elements.point.borderWidth); - break; - case 'label': - for (var i = 0; i < this.lastActive.length; i++) { - dataset = this.data.datasets[this.lastActive[i]._datasetIndex]; - index = this.lastActive[i]._index; - - this.lastActive[i]._model.radius = this.lastActive[i].custom && this.lastActive[i].custom.radius ? this.lastActive[i].custom.radius : helpers.getValueAtIndexOrDefault(dataset.radius, index, this.options.elements.point.radius); - this.lastActive[i]._model.backgroundColor = this.lastActive[i].custom && this.lastActive[i].custom.backgroundColor ? this.lastActive[i].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointBackgroundColor, index, this.options.elements.point.backgroundColor); - this.lastActive[i]._model.borderColor = this.lastActive[i].custom && this.lastActive[i].custom.borderColor ? this.lastActive[i].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.pointBorderColor, index, this.options.elements.point.borderColor); - this.lastActive[i]._model.borderWidth = this.lastActive[i].custom && this.lastActive[i].custom.borderWidth ? this.lastActive[i].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.options.elements.point.borderWidth); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - // Built in hover styling - if (this.active.length && this.options.hover.mode) { - switch (this.options.hover.mode) { - case 'single': - dataset = this.data.datasets[this.active[0]._datasetIndex]; - index = this.active[0]._index; - - this.active[0]._model.radius = this.active[0].custom && this.active[0].custom.radius ? this.active[0].custom.radius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.options.elements.point.hoverRadius); - this.active[0]._model.backgroundColor = this.active[0].custom && this.active[0].custom.hoverBackgroundColor ? this.active[0].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(this.active[0]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - this.active[0]._model.borderColor = this.active[0].custom && this.active[0].custom.hoverBorderColor ? this.active[0].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(this.active[0]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - this.active[0]._model.borderWidth = this.active[0].custom && this.active[0].custom.hoverBorderWidth ? this.active[0].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.active[0]._model.borderWidth); - break; - case 'label': - for (var i = 0; i < this.active.length; i++) { - dataset = this.data.datasets[this.active[i]._datasetIndex]; - index = this.active[i]._index; - - this.active[i]._model.radius = this.active[i].custom && this.active[i].custom.radius ? this.active[i].custom.radius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.options.elements.point.hoverRadius); - this.active[i]._model.backgroundColor = this.active[i].custom && this.active[i].custom.hoverBackgroundColor ? this.active[i].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(this.active[i]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - this.active[i]._model.borderColor = this.active[i].custom && this.active[i].custom.hoverBorderColor ? this.active[i].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(this.active[i]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - this.active[i]._model.borderWidth = this.active[i].custom && this.active[i].custom.hoverBorderWidth ? this.active[i].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.active[i]._model.borderWidth); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - - // Built in Tooltips - if (this.options.tooltips.enabled) { - - // The usual updates - this.tooltip.initialize(); - - // Active - if (this.active.length) { - this.tooltip._model.opacity = 1; - - helpers.extend(this.tooltip, { - _active: this.active, - }); - - this.tooltip.update(); - } else { - // Inactive - this.tooltip._model.opacity = 0; - } - } - - - // Hover animations - this.tooltip.pivot(); - - if (!this.animating) { - var changed; - - helpers.each(this.active, function(element, index) { - if (element !== this.lastActive[index]) { - changed = true; - } - }, this); - - // If entering, leaving, or changing elements, animate the change via pivot - if ((!this.lastActive.length && this.active.length) || - (this.lastActive.length && !this.active.length) || - (this.lastActive.length && this.active.length && changed)) { - - this.stop(); - this.render(this.options.hover.animationDuration); - } - } - - // Remember Last Active - this.lastActive = this.active; - return this; - }, - }); }).call(this); (function() { - "use strict"; - - var root = this, - Chart = root.Chart, - //Cache a local reference to Chart.helpers - helpers = Chart.helpers; - - var defaultConfig = { - - scale: { - type: "radialLinear", - display: true, - - //Boolean - Whether to animate scaling the chart from the centre - animate: false, - - lineArc: true, - - // grid line settings - gridLines: { - show: true, - color: "rgba(0, 0, 0, 0.05)", - lineWidth: 1, - }, - - // scale numbers - beginAtZero: true, - - // label settings - labels: { - show: true, - template: "<%=value.toLocaleString()%>", - fontSize: 12, - fontStyle: "normal", - fontColor: "#666", - fontFamily: "Helvetica Neue", - - //Boolean - Show a backdrop to the scale label - showLabelBackdrop: true, - - //String - The colour of the label backdrop - backdropColor: "rgba(255,255,255,0.75)", - - //Number - The backdrop padding above & below the label in pixels - backdropPaddingY: 2, - - //Number - The backdrop padding to the side of the label in pixels - backdropPaddingX: 2, - } - }, - - //Boolean - Whether to animate the rotation of the chart - animateRotate: true, - }; - - - 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() { - - // Scale setup - var self = this; - var ScaleClass = Chart.scales.getScaleConstructor(this.options.scale.type); - this.scale = new ScaleClass({ - options: this.options.scale, - lineArc: true, - width: this.chart.width, - height: this.chart.height, - xCenter: this.chart.width / 2, - yCenter: this.chart.height / 2, - ctx: this.chart.ctx, - valuesCount: this.data.length, - calculateRange: function() { - this.min = null; - this.max = null; - - helpers.each(self.data.datasets[0].data, function(value) { - if (this.min === null) { - this.min = value; - } else if (value < this.min) { - this.min = value; - } - - if (this.max === null) { - this.max = value; - } else if (value > this.max) { - this.max = value; - } - }, this); - } - }); - - helpers.bindEvents(this, this.options.events, this.events); - - //Set up tooltip events on the chart - helpers.bindEvents(this, this.options.events, this.events); - - //Create a new bar for each piece of data - helpers.each(this.data.datasets, function(dataset, datasetIndex) { - dataset.metaData = []; - helpers.each(dataset.data, function(dataPoint, index) { - dataset.metaData.push(new Chart.Arc({ - _chart: this.chart, - _datasetIndex: datasetIndex, - _index: index, - _model: {} - })); - }, this); - }, this); - - // Create tooltip instance exclusively for this chart with some defaults. - this.tooltip = new Chart.Tooltip({ - _chart: this.chart, - _data: this.data, - _options: this.options, - }, this); - - // Fit the scale before we animate - this.updateScaleRange(); - this.scale.calculateRange(); - Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); - - // so that we animate nicely - this.resetElements(); - - // Update the chart with the latest data. - this.update(); - - }, - updateScaleRange: function() { - helpers.extend(this.scale, { - size: helpers.min([this.chart.width, this.chart.height]), - xCenter: this.chart.width / 2, - yCenter: this.chart.height / 2 - }); - }, - resetElements: function() { - var circumference = 1 / this.data.datasets[0].data.length * 2; - - // Map new data to data points - helpers.each(this.data.datasets[0].metaData, function(slice, index) { - - var value = this.data.datasets[0].data[index]; - - helpers.extend(slice, { - _index: index, - _model: { - x: this.chart.width / 2, - y: this.chart.height / 2, - innerRadius: 0, - outerRadius: 0, - startAngle: Math.PI * -0.5, - endAngle: Math.PI * -0.5, - - backgroundColor: slice.custom && slice.custom.backgroundColor ? slice.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].backgroundColor, index, this.options.elements.arc.backgroundColor), - hoverBackgroundColor: slice.custom && slice.custom.hoverBackgroundColor ? slice.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].hoverBackgroundColor, index, this.options.elements.arc.hoverBackgroundColor), - borderWidth: slice.custom && slice.custom.borderWidth ? slice.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[0].borderWidth, index, this.options.elements.arc.borderWidth), - borderColor: slice.custom && slice.custom.borderColor ? slice.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].borderColor, index, this.options.elements.arc.borderColor), - - label: helpers.getValueAtIndexOrDefault(this.data.datasets[0].labels, index, this.data.datasets[0].labels[index]) - }, - }); - - slice.pivot(); - }, this); - }, - update: function(animationDuration) { - - this.updateScaleRange(); - this.scale.calculateRange(); - this.scale.generateTicks(); - this.scale.buildYLabels(); - - Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); - - var circumference = 1 / this.data.datasets[0].data.length * 2; - - // Map new data to data points - helpers.each(this.data.datasets[0].metaData, function(slice, index) { - - var value = this.data.datasets[0].data[index]; - - var startAngle = (-0.5 * Math.PI) + (Math.PI * circumference) * index; - var endAngle = startAngle + (circumference * Math.PI); - - helpers.extend(slice, { - _index: index, - _model: { - x: this.chart.width / 2, - y: this.chart.height / 2, - innerRadius: 0, - outerRadius: this.scale.calculateCenterOffset(value), - startAngle: startAngle, - endAngle: endAngle, - - backgroundColor: slice.custom && slice.custom.backgroundColor ? slice.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].backgroundColor, index, this.options.elements.arc.backgroundColor), - hoverBackgroundColor: slice.custom && slice.custom.hoverBackgroundColor ? slice.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].hoverBackgroundColor, index, this.options.elements.arc.hoverBackgroundColor), - borderWidth: slice.custom && slice.custom.borderWidth ? slice.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.data.datasets[0].borderWidth, index, this.options.elements.arc.borderWidth), - borderColor: slice.custom && slice.custom.borderColor ? slice.custom.borderColor : helpers.getValueAtIndexOrDefault(this.data.datasets[0].borderColor, index, this.options.elements.arc.borderColor), - - label: helpers.getValueAtIndexOrDefault(this.data.datasets[0].labels, index, this.data.datasets[0].labels[index]) - }, - }); - slice.pivot(); - - console.log(slice); - - }, this); - - this.render(animationDuration); - }, - draw: function(ease) { - var easingDecimal = ease || 1; - - this.clear(); - - helpers.each(this.data.datasets[0].metaData, function(slice, index) { - slice.transition(easingDecimal).draw(); - }, this); - - this.scale.draw(); - - this.tooltip.transition(easingDecimal).draw(); - }, - events: function(e) { - - // If exiting chart - if (e.type == 'mouseout') { - return this; - } - - this.lastActive = this.lastActive || []; - - // Find Active Elements - this.active = function() { - switch (this.options.hover.mode) { - case 'single': - return this.getSliceAtEvent(e); - case 'label': - return this.getSlicesAtEvent(e); - case 'dataset': - return this.getDatasetAtEvent(e); - default: - return e; - } - }.call(this); - - // On Hover hook - if (this.options.hover.onHover) { - this.options.hover.onHover.call(this, this.active); - } - - if (e.type == 'mouseup' || e.type == 'click') { - if (this.options.onClick) { - this.options.onClick.call(this, e, this.active); - } - } - - var dataset; - var index; - // Remove styling for last active (even if it may still be active) - if (this.lastActive.length) { - switch (this.options.hover.mode) { - case 'single': - dataset = this.data.datasets[this.lastActive[0]._datasetIndex]; - index = this.lastActive[0]._index; - - this.lastActive[0]._model.backgroundColor = this.lastActive[0].custom && this.lastActive[0].custom.backgroundColor ? this.lastActive[0].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.arc.backgroundColor); - this.lastActive[0]._model.borderColor = this.lastActive[0].custom && this.lastActive[0].custom.borderColor ? this.lastActive[0].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.arc.borderColor); - this.lastActive[0]._model.borderWidth = this.lastActive[0].custom && this.lastActive[0].custom.borderWidth ? this.lastActive[0].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.arc.borderWidth); - break; - case 'label': - for (var i = 0; i < this.lastActive.length; i++) { - dataset = this.data.datasets[this.lastActive[i]._datasetIndex]; - index = this.lastActive[i]._index; - - this.lastActive[i]._model.backgroundColor = this.lastActive[i].custom && this.lastActive[i].custom.backgroundColor ? this.lastActive[i].custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, this.options.elements.arc.backgroundColor); - this.lastActive[i]._model.borderColor = this.lastActive[i].custom && this.lastActive[i].custom.borderColor ? this.lastActive[i].custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, this.options.elements.arc.borderColor); - this.lastActive[i]._model.borderWidth = this.lastActive[i].custom && this.lastActive[i].custom.borderWidth ? this.lastActive[i].custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, this.options.elements.arc.borderWidth); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - // Built in hover styling - if (this.active.length && this.options.hover.mode) { - switch (this.options.hover.mode) { - case 'single': - dataset = this.data.datasets[this.active[0]._datasetIndex]; - index = this.active[0]._index; - - this.active[0]._model.radius = this.active[0].custom && this.active[0].custom.hoverRadius ? this.active[0].custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.active[0]._model.radius + 1); - this.active[0]._model.backgroundColor = this.active[0].custom && this.active[0].custom.hoverBackgroundColor ? this.active[0].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(this.active[0]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - this.active[0]._model.borderColor = this.active[0].custom && this.active[0].custom.hoverBorderColor ? this.active[0].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(this.active[0]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - this.active[0]._model.borderWidth = this.active[0].custom && this.active[0].custom.hoverBorderWidth ? this.active[0].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.active[0]._model.borderWidth); - break; - case 'label': - for (var i = 0; i < this.active.length; i++) { - dataset = this.data.datasets[this.active[i]._datasetIndex]; - index = this.active[i]._index; - - this.active[i]._model.radius = this.active[i].custom && this.active[i].custom.hoverRadius ? this.active[i].custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.active[i]._model.radius + 1); - this.active[i]._model.backgroundColor = this.active[i].custom && this.active[i].custom.hoverBackgroundColor ? this.active[i].custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.color(this.active[i]._model.backgroundColor).saturate(0.5).darken(0.1).rgbString()); - this.active[i]._model.borderColor = this.active[i].custom && this.active[i].custom.hoverBorderColor ? this.active[i].custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.color(this.active[i]._model.borderColor).saturate(0.5).darken(0.1).rgbString()); - this.active[i]._model.borderWidth = this.active[i].custom && this.active[i].custom.hoverBorderWidth ? this.active[i].custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, this.active[i]._model.borderWidth); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - - // Built in Tooltips - if (this.options.tooltips.enabled) { - - // The usual updates - this.tooltip.initialize(); - - // Active - if (this.active.length) { - this.tooltip._model.opacity = 1; - - helpers.extend(this.tooltip, { - _active: this.active, - }); - - this.tooltip.update(); - } else { - // Inactive - this.tooltip._model.opacity = 0; - } - } - - - // Hover animations - this.tooltip.pivot(); - - if (!this.animating) { - var changed; - - helpers.each(this.active, function(element, index) { - if (element !== this.lastActive[index]) { - changed = true; - } - }, this); - - // If entering, leaving, or changing elements, animate the change via pivot - if ((!this.lastActive.length && this.active.length) || - (this.lastActive.length && !this.active.length) || - (this.lastActive.length && this.active.length && changed)) { - - this.stop(); - this.render(this.options.hover.animationDuration); - } - } - - // Remember Last Active - this.lastActive = this.active; - return this; - }, - getSliceAtEvent: function(e) { - var elements = []; - - var location = helpers.getRelativePosition(e); - - this.eachElement(function(slice, index) { - if (slice.inRange(location.x, location.y)) { - elements.push(slice); - } - }, this); - return elements; - }, - /*getSlicesAtEvent: function(e) { - var elements = []; - - var location = helpers.getRelativePosition(e); - - this.eachElement(function(slice, index) { - if (slice.inGroupRange(location.x, location.y)) { - elements.push(slice); - } - }, this); - return elements; - },*/ - }); - -}).call(this); - -(function() { - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - - - Chart.Type.extend({ - name: "Radar", - defaults: { - - scale: { - type: "radialLinear", - display: true, - - //Boolean - Whether to animate scaling the chart from the centre - animate: false, - - lineArc: false, - - // grid line settings - gridLines: { - show: true, - color: "rgba(0, 0, 0, 0.05)", - lineWidth: 1, - }, - - angleLines: { - show: true, - color: "rgba(0,0,0,.1)", - lineWidth: 1 - }, - - // scale numbers - beginAtZero: true, - - // label settings - labels: { - show: true, - template: "<%=value.toLocaleString()%>", - fontSize: 12, - fontStyle: "normal", - fontColor: "#666", - fontFamily: "Helvetica Neue", - - //Boolean - Show a backdrop to the scale label - showLabelBackdrop: true, - - //String - The colour of the label backdrop - backdropColor: "rgba(255,255,255,0.75)", - - //Number - The backdrop padding above & below the label in pixels - backdropPaddingY: 2, - - //Number - The backdrop padding to the side of the label in pixels - backdropPaddingX: 2, - }, - - pointLabels: { - //String - Point label font declaration - fontFamily: "'Arial'", - - //String - Point label font weight - fontStyle: "normal", - - //Number - Point label font size in pixels - fontSize: 10, - - //String - Point label font colour - fontColor: "#666", - }, - }, - - elements: { - line: { - tension: 0, // no bezier in radar - } - }, - - //String - A legend template - legendTemplate: "
g&&(g=t.x+a,s=e):e>this.valuesCount/2&&t.x-a
0){var n=this.getDistanceFromCenterForValue(this.ticks[a]),s=this.yCenter-n;if(this.options.gridLines.show)if(t.strokeStyle=this.options.gridLines.color,t.lineWidth=this.options.gridLines.lineWidth,this.options.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var o=0;o<% for (var i=0; i
'},initialize:function(){i.bindEvents(this,this.options.events,this.events),i.each(this.data.datasets,function(t,o){t.metaDataset=new e.Line({_chart:this.chart,_datasetIndex:o,_points:t.metaData,_loop:!0}),t.metaData=[],i.each(t.data,function(i,s){t.metaData.push(new e.Point({_datasetIndex:o,_index:s,_chart:this.chart,_model:{x:0,y:0}}))},this)},this),this.buildScale(),this.tooltip=new e.Tooltip({_chart:this.chart,_data:this.data,_options:this.options},this),e.scaleService.fitScalesForChart(this,this.chart.width,this.chart.height),this.resetElements(),this.update()},nextPoint:function(t,e){return t[e+1]||t[0]},previousPoint:function(t,e){return t[e-1]||t[t.length-1]},resetElements:function(){this.eachElement(function(t,e,o,s){i.extend(t,{_chart:this.chart,_datasetIndex:s,_index:e,_scale:this.scale,_model:{x:this.scale.xCenter,y:this.scale.yCenter,tension:t.custom&&t.custom.tension?t.custom.tension:this.options.elements.line.tension,radius:t.custom&&t.custom.radius?t.custom.pointRadius:i.getValueAtIndexOrDefault(this.data.datasets[s].pointRadius,e,this.options.elements.point.radius),backgroundColor:t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:i.getValueAtIndexOrDefault(this.data.datasets[s].pointBackgroundColor,e,this.options.elements.point.backgroundColor),borderColor:t.custom&&t.custom.borderColor?t.custom.borderColor:i.getValueAtIndexOrDefault(this.data.datasets[s].pointBorderColor,e,this.options.elements.point.borderColor),borderWidth:t.custom&&t.custom.borderWidth?t.custom.borderWidth:i.getValueAtIndexOrDefault(this.data.datasets[s].pointBorderWidth,e,this.options.elements.point.borderWidth),skip:null===this.data.datasets[s].data[e],hitRadius:t.custom&&t.custom.hitRadius?t.custom.hitRadius:i.getValueAtIndexOrDefault(this.data.datasets[s].hitRadius,e,this.options.elements.point.hitRadius)}})},this),this.eachElement(function(t,e,o){i.splineCurve(this.previousPoint(o,e)._model,t._model,this.nextPoint(o,e)._model,t._model.tension);t._model.controlPointPreviousX=this.scale.xCenter,t._model.controlPointPreviousY=this.scale.yCenter,t._model.controlPointNextX=this.scale.xCenter,t._model.controlPointNextY=this.scale.yCenter,t.pivot()},this)},update:function(t){e.scaleService.fitScalesForChart(this,this.chart.width,this.chart.height),this.eachDataset(function(t,e){var o;o=this.scale.min<0&&this.scale.max<0?this.scale.getPointPosition(0,this.scale.max):this.scale.min>0&&this.scale.max>0?this.scale.getPointPosition(0,this.scale.min):this.scale.getPointPosition(0,0),i.extend(t.metaDataset,{_datasetIndex:e,_children:t.metaData,_model:{tension:t.tension||this.options.elements.line.tension,backgroundColor:t.backgroundColor||this.options.elements.line.backgroundColor,borderWidth:t.borderWidth||this.options.elements.line.borderWidth,borderColor:t.borderColor||this.options.elements.line.borderColor,fill:void 0!==t.fill?t.fill:this.options.elements.line.fill,skipNull:void 0!==t.skipNull?t.skipNull:this.options.elements.line.skipNull,drawNull:void 0!==t.drawNull?t.drawNull:this.options.elements.line.drawNull,scaleTop:this.scale.top,scaleBottom:this.scale.bottom,scaleZero:o}}),t.metaDataset.pivot()}),this.eachElement(function(t,e,o,s){var a=this.scale.getPointPosition(e,this.scale.calculateCenterOffset(this.data.datasets[s].data[e]));i.extend(t,{_chart:this.chart,_datasetIndex:s,_index:e,_model:{x:a.x,y:a.y,tension:t.custom&&t.custom.tension?t.custom.tension:this.options.elements.line.tension,radius:t.custom&&t.custom.radius?t.custom.pointRadius:i.getValueAtIndexOrDefault(this.data.datasets[s].pointRadius,e,this.options.elements.point.radius),backgroundColor:t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:i.getValueAtIndexOrDefault(this.data.datasets[s].pointBackgroundColor,e,this.options.elements.point.backgroundColor),borderColor:t.custom&&t.custom.borderColor?t.custom.borderColor:i.getValueAtIndexOrDefault(this.data.datasets[s].pointBorderColor,e,this.options.elements.point.borderColor),borderWidth:t.custom&&t.custom.borderWidth?t.custom.borderWidth:i.getValueAtIndexOrDefault(this.data.datasets[s].pointBorderWidth,e,this.options.elements.point.borderWidth),skip:null===this.data.datasets[s].data[e],hitRadius:t.custom&&t.custom.hitRadius?t.custom.hitRadius:i.getValueAtIndexOrDefault(this.data.datasets[s].hitRadius,e,this.options.elements.point.hitRadius)}})},this),this.eachElement(function(t,e,o){var s=i.splineCurve(this.previousPoint(o,e)._model,t._model,this.nextPoint(o,e)._model,t._model.tension);t._model.controlPointPreviousX=s.previous.x,t._model.controlPointNextX=s.next.x,t._model.controlPointNextY=s.next.y>this.chartArea.bottom?this.chartArea.bottom:s.next.y<% for (var i=0; i
',tooltips:{template:"(<%= value.x %>, <%= value.y %>)",multiTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%>(<%= value.x %>, <%= value.y %>)"}};e.Type.extend({name:"Scatter",defaults:o,initialize:function(){i.bindEvents(this,this.options.events,this.events),i.each(this.data.datasets,function(t,o){t.metaDataset=new e.Line({_chart:this.chart,_datasetIndex:o,_points:t.metaData}),t.metaData=[],i.each(t.data,function(i,s){t.metaData.push(new e.Point({_datasetIndex:o,_index:s,_chart:this.chart,_model:{x:0,y:0}}))},this),t.xAxisID||(t.xAxisID=this.options.scales.xAxes[0].id),t.yAxisID||(t.yAxisID=this.options.scales.yAxes[0].id)},this),this.buildScale(),this.tooltip=new e.Tooltip({_chart:this.chart,_data:this.data,_options:this.options},this),e.scaleService.fitScalesForChart(this,this.chart.width,this.chart.height),this.resetElements(),this.update()},nextPoint:function(t,e){return t[e+1]||t[e]},previousPoint:function(t,e){return t[e-1]||t[e]},resetElements:function(){this.eachElement(function(t,e,o,s){var a,r=this.scales[this.data.datasets[s].xAxisID],n=this.scales[this.data.datasets[s].yAxisID];a=n.getPixelForValue(n.min<0&&n.max<0?n.max:n.min>0&&n.max>0?n.min:0),i.extend(t,{_chart:this.chart,_xScale:r,_yScale:n,_datasetIndex:s,_index:e,_model:{x:r.getPixelForValue(this.data.datasets[s].data[e].x),y:a,tension:t.custom&&t.custom.tension?t.custom.tension:this.options.elements.line.tension,radius:t.custom&&t.custom.radius?t.custom.pointRadius:i.getValueAtIndexOrDefault(this.data.datasets[s].pointRadius,e,this.options.elements.point.radius),backgroundColor:t.custom&&t.custom.backgroundColor?t.custom.backgroundColor:i.getValueAtIndexOrDefault(this.data.datasets[s].pointBackgroundColor,e,this.options.elements.point.backgroundColor),borderColor:t.custom&&t.custom.borderColor?t.custom.borderColor:i.getValueAtIndexOrDefault(this.data.datasets[s].pointBorderColor,e,this.options.elements.point.borderColor),borderWidth:t.custom&&t.custom.borderWidth?t.custom.borderWidth:i.getValueAtIndexOrDefault(this.data.datasets[s].pointBorderWidth,e,this.options.elements.point.borderWidth),skip:null===this.data.datasets[s].data[e]||null===this.data.datasets[s].data[e].x||null===this.data.datasets[s].data[e].y,hoverRadius:t.custom&&t.custom.hoverRadius?t.custom.hoverRadius:i.getValueAtIndexOrDefault(this.data.datasets[s].pointHitRadius,e,this.options.elements.point.hitRadius)}})},this),this.eachElement(function(t,e,o){var s=i.splineCurve(this.previousPoint(o,e)._model,t._model,this.nextPoint(o,e)._model,t._model.tension);t._model.controlPointPreviousX=s.previous.x,t._model.controlPointNextX=s.next.x,t._model.controlPointNextY=s.next.y>this.chartArea.bottom?this.chartArea.bottom:s.next.y