diff --git a/Chart.js b/Chart.js index 35a27e8db..eff3934de 100644 --- a/Chart.js +++ b/Chart.js @@ -19,7 +19,6 @@ //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 @@ -32,32 +31,40 @@ context = context.getContext("2d"); } + this.ctx = context; this.canvas = context.canvas; - this.ctx = context; + // Figure out what the size of the chart will be. + // If the canvas has a specified width and height, we use those else + // we look to see if the canvas node has a CSS width and height. + // If there is still no height, fill the parent container + this.width = context.canvas.width || parseInt(Chart.helpers.getStyle(context.canvas, 'width')) || Chart.helpers.getMaximumWidth(context.canvas); + this.height = context.canvas.height || parseInt(Chart.helpers.getStyle(context.canvas, 'height')) || Chart.helpers.getMaximumHeight(context.canvas); - //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. + + if (isNaN(this.aspectRatio) || isFinite(this.aspectRatio) === false) { + // If the canvas has no size, try and figure out what the aspect ratio will be. + // Some charts prefer square canvases (pie, radar, etc). If that is specified, use that + // else use the canvas default ratio of 2 + this.aspectRatio = config.aspectRatio !== undefined ? config.aspectRatio : 2; + } + + // Store the original style of the element so we can set it back + this.originalCanvasStyleWidth = context.canvas.style.width; + this.originalCanvasStyleHeight = context.canvas.style.height; + + // High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. Chart.helpers.retinaScale(this); + // Always bind this so that if the responsive state changes we still work + var _this = this; + Chart.helpers.addResizeListener(context.canvas.parentNode, function() { + if (config.options.responsive) { + _this.controller.resize(); + } + }); + if (config) { this.controller = new Chart.Controller(this); return this.controller; @@ -213,6 +220,24 @@ return base; }, + extendDeep = helpers.extendDeep = function(_base) { + return _extendDeep.apply(this, arguments); + + function _extendDeep(dst) { + helpers.each(arguments, function(obj) { + if (obj !== dst) { + helpers.each(obj, function(value, key) { + if (dst[key] && dst[key].constructor && dst[key].constructor === Object) { + _extendDeep(dst[key], value); + } else { + dst[key] = value; + } + }); + } + }); + return dst; + } + }, scaleMerge = helpers.scaleMerge = function(_base, extension) { var base = clone(_base); @@ -496,8 +521,7 @@ } else { // Generate a reusable function that will serve as a template // generator (and which will be cached). - fn = new Function("obj", - "var p=[],print=function(){p.push.apply(p,arguments);};" + + var functionCode = "var p=[],print=function(){p.push.apply(p,arguments);};" + // Introduce the data as local variables using with(){} "with(obj){p.push('" + @@ -511,8 +535,8 @@ .split("\t").join("');") .split("%>").join("p.push('") .split("\r").join("\\'") + - "');}return p.join('');" - ); + "');}return p.join('');"; + fn = new Function("obj", functionCode); // Cache the result templateStringCache[str] = fn; @@ -745,21 +769,30 @@ }; })(), //-- DOM methods - getRelativePosition = helpers.getRelativePosition = function(evt) { + getRelativePosition = helpers.getRelativePosition = function(evt, chart) { 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; + mouseX = e.touches[0].clientX; + mouseY = e.touches[0].clientY; } else { - mouseX = e.clientX - boundingRect.left; - mouseY = e.clientY - boundingRect.top; + mouseX = e.clientX; + mouseY = e.clientY; } + // Scale mouse coordinates into canvas coordinates + // by following the pattern laid out by 'jerryj' in the comments of + // http://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/ + + // We divide by the current device pixel ratio, because the canvas is scaled up by that amount in each direction. However + // the backend model is in unscaled coordinates. Since we are going to deal with our model coordinates, we go back here + mouseX = Math.round((mouseX - boundingRect.left) / (boundingRect.right - boundingRect.left) * canvas.width / chart.currentDevicePixelRatio); + mouseY = Math.round((mouseY - boundingRect.top) / (boundingRect.bottom - boundingRect.top) * canvas.height / chart.currentDevicePixelRatio); + return { x: mouseX, y: mouseY @@ -800,17 +833,54 @@ removeEvent(chartInstance.chart.canvas, eventName, handler); }); }, + getConstraintWidth = helpers.getConstraintWidth = function(domNode) { // returns Number or undefined if no constraint + var constrainedWidth; + var constrainedWNode = document.defaultView.getComputedStyle(domNode)['max-width']; + var constrainedWContainer = document.defaultView.getComputedStyle(domNode.parentNode)['max-width']; + var hasCWNode = constrainedWNode !== null && constrainedWNode !== "none"; + var hasCWContainer = constrainedWContainer !== null && constrainedWContainer !== "none"; + + if (hasCWNode || hasCWContainer) { + constrainedWidth = Math.min((hasCWNode ? parseInt(constrainedWNode, 10) : Number.POSITIVE_INFINITY), (hasCWContainer ? parseInt(constrainedWContainer, 10) : Number.POSITIVE_INFINITY)); + } + return constrainedWidth; + }, + getConstraintHeight = helpers.getConstraintHeight = function(domNode) { // returns Number or undefined if no constraint + + var constrainedHeight; + var constrainedHNode = document.defaultView.getComputedStyle(domNode)['max-height']; + var constrainedHContainer = document.defaultView.getComputedStyle(domNode.parentNode)['max-height']; + var hasCHNode = constrainedHNode !== null && constrainedHNode !== "none"; + var hasCHContainer = constrainedHContainer !== null && constrainedHContainer !== "none"; + + if (constrainedHNode || constrainedHContainer) { + constrainedHeight = Math.min((hasCHNode ? parseInt(constrainedHNode, 10) : Number.POSITIVE_INFINITY), (hasCHContainer ? parseInt(constrainedHContainer, 10) : Number.POSITIVE_INFINITY)); + } + return constrainedHeight; + }, 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; + var container = domNode.parentNode; + var padding = parseInt(getStyle(container, 'padding-left')) + parseInt(getStyle(container, 'padding-right')); + + var w = container.clientWidth - padding; + var cw = getConstraintWidth(domNode); + if (cw !== undefined) { + w = Math.min(w, cw); + } + + return w; }, 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; + var container = domNode.parentNode; + var padding = parseInt(getStyle(container, 'padding-top')) + parseInt(getStyle(container, 'padding-bottom')); + + var h = container.clientHeight - padding; + var ch = getConstraintHeight(domNode); + if (ch !== undefined) { + h = Math.min(h, ch); + } + + return h; }, getStyle = helpers.getStyle = function(el, property) { return el.currentStyle ? @@ -819,16 +889,23 @@ }, getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support retinaScale = helpers.retinaScale = function(chart) { - var ctx = chart.ctx, - width = chart.canvas.width, - height = chart.canvas.height; + var ctx = chart.ctx; + var width = chart.canvas.width; + var height = chart.canvas.height; + chart.currentDevicePixelRatio = window.devicePixelRatio || 1; - if (window.devicePixelRatio) { - ctx.canvas.style.width = width + "px"; - ctx.canvas.style.height = height + "px"; + if (window.devicePixelRatio !== 1) { ctx.canvas.height = height * window.devicePixelRatio; ctx.canvas.width = width * window.devicePixelRatio; ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + + ctx.canvas.style.width = width + 'px'; + ctx.canvas.style.height = height + 'px'; + + // Store the device pixel ratio so that we can go backwards in `destroy`. + // The devicePixelRatio changes with zoom, so there are no guarantees that it is the same + // when destroy is called + chart.originalDevicePixelRatio = chart.originalDevicePixelRatio || window.devicePixelRatio; } }, //-- Canvas methods @@ -867,6 +944,48 @@ } return window.Color(color); }, + addResizeListener = helpers.addResizeListener = function(node, callback) { + // Hide an iframe before the node + var hiddenIframe = document.createElement('iframe'); + var hiddenIframeClass = 'chartjs-hidden-iframe'; + + if (hiddenIframe.classlist) { + // can use classlist + hiddenIframe.classlist.add(hiddenIframeClass); + } else { + hiddenIframe.setAttribute('class', hiddenIframeClass) + } + + // Set the style + hiddenIframe.style.width = '100%'; + hiddenIframe.style.display = 'block'; + hiddenIframe.style.border = 0; + hiddenIframe.style.height = 0; + hiddenIframe.style.margin = 0; + hiddenIframe.style.position = 'absolute'; + hiddenIframe.style.left = 0; + hiddenIframe.style.right = 0; + hiddenIframe.style.top = 0; + hiddenIframe.style.bottom = 0; + + // Insert the iframe so that contentWindow is available + node.insertBefore(hiddenIframe, node.firstChild); + + var timer = 0; + (hiddenIframe.contentWindow || hiddenIframe).onresize = function() { + if (callback) { + callback(); + } + } + }, + removeResizeListener = helpers.removeResizeListener = function(node) { + var hiddenIframe = node.querySelector('.chartjs-hidden-iframe'); + + // Remove the resize detect iframe + if (hiddenIframe) { + hiddenIframe.remove(); + } + }, isArray = helpers.isArray = function(obj) { if (!Array.isArray) { return Object.prototype.toString.call(arg) === '[object Array]'; @@ -1159,57 +1278,11 @@ return this; }, - addDataset: function addDataset(dataset, index) { - if (index !== undefined) { - this.data.datasets.splice(index, 0, dataset); - } else { - this.data.datasets.push(dataset); - } - - this.buildOrUpdateControllers(); - dataset.controller.reset(); // so that animation looks ok - this.update(); - }, - removeDataset: function removeDataset(index) { - this.data.datasets.splice(index, 1); - this.buildOrUpdateControllers(); - this.update(); - }, - - // Add data to the given dataset - // @param data: the data to add - // @param {Number} datasetIndex : the index of the dataset to add to - // @param {Number} index : the index of the data - addData: function addData(data, datasetIndex, index) { - if (datasetIndex < this.data.datasets.length) { - if (index === undefined) { - index = this.data.datasets[datasetIndex].data.length; - } - - var addElementArgs = [index]; - for (var i = 3; i < arguments.length; ++i) { - addElementArgs.push(arguments[i]); - } - - this.data.datasets[datasetIndex].data.splice(index, 0, data); - this.data.datasets[datasetIndex].controller.addElementAndReset.apply(this.data.datasets[datasetIndex].controller, addElementArgs); - this.update(); - } - }, - - removeData: function removeData(datasetIndex, index) { - if (datasetIndex < this.data.datasets.length) { - this.data.datasets[datasetIndex].data.splice(index, 1); - this.data.datasets[datasetIndex].controller.removeElement(index); - this.update(); - } - }, - resize: function resize(silent) { this.stop(); - var canvas = this.chart.canvas, - newWidth = helpers.getMaximumWidth(this.chart.canvas), - newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : helpers.getMaximumHeight(this.chart.canvas); + var canvas = this.chart.canvas; + var newWidth = helpers.getMaximumWidth(this.chart.canvas); + var newHeight = (this.options.maintainAspectRatio && isNaN(this.chart.aspectRatio) === false && isFinite(this.chart.aspectRatio) && this.chart.aspectRatio !== 0) ? newWidth / this.chart.aspectRatio : helpers.getMaximumHeight(this.chart.canvas); canvas.width = this.chart.width = newWidth; canvas.height = this.chart.height = newHeight; @@ -1289,18 +1362,35 @@ this.scale = scale; } - Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); + Chart.scaleService.update(this, this.chart.width, this.chart.height); }, - buildOrUpdateControllers: function() { + buildOrUpdateControllers: function buildOrUpdateControllers(resetNewControllers) { + var types = []; helpers.each(this.data.datasets, function(dataset, datasetIndex) { - var type = dataset.type || this.config.type; + if (!dataset.type) { + dataset.type = this.config.type; + } + var type = dataset.type; + types.push(type); if (dataset.controller) { dataset.controller.updateIndex(datasetIndex); return; } dataset.controller = new Chart.controllers[type](this, datasetIndex); + + if (resetNewControllers) { + dataset.controller.reset(); + } }, this); + if (types.length > 1) { + for (var i = 1; i < types.length; i++) { + if (types[i] != types[i - 1]) { + this.isCombo = true; + break; + } + } + } }, resetElements: function resetElements() { @@ -1309,10 +1399,18 @@ }, this); }, - update: function update(animationDuration, lazy) { + Chart.scaleService.update(this, this.chart.width, this.chart.height); + + // Make sure dataset controllers are updated and new controllers are reset + this.buildOrUpdateControllers(true); + + // Make sure all dataset controllers have correct meta data counts + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + dataset.controller.buildOrUpdateElements(); + }, this); + // 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); helpers.each(this.data.datasets, function(dataset, datasetIndex) { dataset.controller.update(); }, this); @@ -1370,15 +1468,11 @@ this.tooltip.transition(easingDecimal).draw(); }, - - - - // 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(e) { - var eventPosition = helpers.getRelativePosition(e); + var eventPosition = helpers.getRelativePosition(e, this.chart); var elementsArray = []; helpers.each(this.data.datasets, function(dataset, datasetIndex) { @@ -1394,7 +1488,7 @@ }, getElementsAtEvent: function(e) { - var eventPosition = helpers.getRelativePosition(e); + var eventPosition = helpers.getRelativePosition(e, this.chart); var elementsArray = []; helpers.each(this.data.datasets, function(dataset, datasetIndex) { @@ -1409,13 +1503,15 @@ }, getDatasetAtEvent: function(e) { - var eventPosition = helpers.getRelativePosition(e); + var eventPosition = helpers.getRelativePosition(e, this.chart); var elementsArray = []; - 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].inLabelRange(eventPosition.x, eventPosition.y)) { - helpers.each(this.chart.data.datasets, datasetIterator); + 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].inLabelRange(eventPosition.x, eventPosition.y)) { + helpers.each(this.data.datasets[datasetIndex].metaData, function(element, index) { + elementsArray.push(element); + }, this); } } } @@ -1430,21 +1526,22 @@ destroy: function destroy() { this.clear(); helpers.unbindEvents(this, this.events); - var canvas = this.chart.canvas; + helpers.removeResizeListener(this.chart.canvas.parentNode); - // Reset canvas height/width attributes starts a fresh with the canvas context + // Reset canvas height/width attributes + var canvas = this.chart.canvas; 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'); + // if we scaled the canvas in response to a devicePixelRatio !== 1, we need to undo that transform here + if (this.chart.originalDevicePixelRatio !== undefined) { + this.chart.ctx.scale(1 / this.chart.originalDevicePixelRatio, 1 / this.chart.originalDevicePixelRatio); } + // Reset to the old style since it may have been changed by the device pixel ratio changes + canvas.style.width = this.chart.originalCanvasStyleWidth; + canvas.style.height = this.chart.originalCanvasStyleHeight; + delete Chart.instances[this.id]; }, @@ -1506,12 +1603,11 @@ this.data.datasets[this.lastActive[0]._datasetIndex].controller.removeHoverStyle(this.lastActive[0], this.lastActive[0]._datasetIndex, this.lastActive[0]._index); break; case 'label': + case 'dataset': for (var i = 0; i < this.lastActive.length; i++) { this.data.datasets[this.lastActive[i]._datasetIndex].controller.removeHoverStyle(this.lastActive[i], this.lastActive[i]._datasetIndex, this.lastActive[i]._index); } break; - case 'dataset': - break; default: // Don't change anything } @@ -1524,12 +1620,11 @@ this.data.datasets[this.active[0]._datasetIndex].controller.setHoverStyle(this.active[0]); break; case 'label': + case 'dataset': for (var i = 0; i < this.active.length; i++) { this.data.datasets[this.active[i]._datasetIndex].controller.setHoverStyle(this.active[i]); } break; - case 'dataset': - break; default: // Don't change anything } @@ -1575,7 +1670,7 @@ (this.lastActive.length && this.active.length && changed)) { this.stop(); - + // We only need to render at this point. Updating will cause scales to be recomputed generating flicker & using more // memory than necessary. this.render(this.options.hover.animationDuration, true); @@ -1591,32 +1686,472 @@ }).call(this); (function() { - "use strict"; - //Declare root variable - window in the browser, global on the server var root = this, - previous = root.Chart, + Chart = root.Chart, helpers = Chart.helpers; + Chart.defaults.scale = { + display: true, - // 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() { - helpers.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(); + // 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, + }, + + // label settings + ticks: { + show: true, + minRotation: 20, + maxRotation: 90, + template: "<%=value%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue", + }, + }; + + Chart.Scale = Chart.Element.extend({ + + // These methods are ordered by lifecyle. Utilities then follow. + // Any function defined here is inherited by all scale types. + // Any function can be extended by the scale type + + beforeUpdate: helpers.noop, + update: function(maxWidth, maxHeight, margins) { + + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + this.beforeUpdate(); + + // Absorb the master measurements + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + this.margins = margins; + + // Dimensions + this.beforeSetDimensions(); + this.setDimensions(); + this.afterSetDimensions(); + // Ticks + this.beforeBuildTicks(); + this.buildTicks(); + this.afterBuildTicks(); + // Tick Rotation + this.beforeCalculateTickRotation(); + this.calculateTickRotation(); + this.afterCalculateTickRotation(); + // Fit + this.beforeFit(); + this.fit(); + this.afterFit(); + // + this.afterUpdate(); + + return this.minSize; + + }, + afterUpdate: helpers.noop, + + // + + beforeSetDimensions: helpers.noop, + setDimensions: function() { + // Set the unconstrained dimension before label rotation + if (this.isHorizontal()) { + this.width = this.maxWidth; + } else { + this.height = this.maxHeight; + } + }, + afterSetDimensions: helpers.noop, + + // + + beforeBuildTicks: helpers.noop, + buildTicks: helpers.noop, + afterBuildTicks: helpers.noop, + + // + + beforeCalculateTickRotation: helpers.noop, + calculateTickRotation: function() { + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + var labelFont = helpers.fontString(this.options.ticks.fontSize, this.options.ticks.fontStyle, this.options.ticks.fontFamily); + this.ctx.font = labelFont; + + var firstWidth = this.ctx.measureText(this.ticks[0]).width; + var lastWidth = this.ctx.measureText(this.ticks[this.ticks.length - 1]).width; + var firstRotated; + var lastRotated; + + this.paddingRight = lastWidth / 2 + 3; + this.paddingLeft = firstWidth / 2 + 3; + + this.labelRotation = 0; + + if (this.options.display && this.isHorizontal()) { + var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.ticks); + var cosRotation; + var sinRotation; + var firstRotatedWidth; + + 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 tickWidth = this.getPixelForTick(1) - this.getPixelForTick(0) - 6; + + //Max label rotation can be set or default to 90 - also act as a loop counter + while (this.labelWidth > tickWidth && this.labelRotation <= this.options.ticks.maxRotation) { + 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.ticks.fontSize / 2 > this.yLabelWidth) { + this.paddingLeft = firstRotated + this.options.ticks.fontSize / 2; } - }); - }, 16); - }; - })()); + + this.paddingRight = this.options.ticks.fontSize / 2; + + if (sinRotation * originalLabelWidth > this.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 (this.margins) { + this.paddingLeft -= this.margins.left; + this.paddingRight -= this.margins.right; + + this.paddingLeft = Math.max(this.paddingLeft, 0); + this.paddingRight = Math.max(this.paddingRight, 0); + } + }, + afterCalculateTickRotation: helpers.noop, + + // + + beforeFit: helpers.noop, + fit: function() { + + this.minSize = { + width: 0, + height: 0, + }; + + // Width + if (this.isHorizontal()) { + this.minSize.width = this.maxWidth; // fill all the width + } else { + this.minSize.width = this.options.gridLines.show && this.options.display ? 10 : 0; + } + + // height + if (this.isHorizontal()) { + this.minSize.height = this.options.gridLines.show && this.options.display ? 10 : 0; + } else { + this.minSize.height = this.maxHeight; // fill all the height + } + + this.paddingLeft = 0; + this.paddingRight = 0; + this.paddingTop = 0; + this.paddingBottom = 0; + + if (this.options.ticks.show && this.options.display) { + // Don't bother fitting the ticks if we are not showing them + var labelFont = helpers.fontString(this.options.ticks.fontSize, + this.options.ticks.fontStyle, this.options.ticks.fontFamily); + + if (this.isHorizontal()) { + // A horizontal axis is more constrained by the height. + var maxLabelHeight = this.maxHeight - this.minSize.height; + var labelHeight = 1.5 * this.options.ticks.fontSize; + this.minSize.height = Math.min(this.maxHeight, this.minSize.height + labelHeight); + + labelFont = helpers.fontString(this.options.ticks.fontSize, this.options.ticks.fontStyle, this.options.ticks.fontFamily); + this.ctx.font = labelFont; + + var firstLabelWidth = this.ctx.measureText(this.ticks[0]).width; + var lastLabelWidth = this.ctx.measureText(this.ticks[this.ticks.length - 1]).width; + + // Ensure that our ticks are always inside the canvas + this.paddingLeft = firstLabelWidth / 2; + this.paddingRight = lastLabelWidth / 2; + } else { + // A vertical axis is more constrained by the width. Labels are the dominant factor here, so get that length first + var maxLabelWidth = this.maxWidth - this.minSize.width; + var largestTextWidth = helpers.longestText(this.ctx, labelFont, this.ticks); + + if (largestTextWidth < maxLabelWidth) { + // We don't need all the room + this.minSize.width += largestTextWidth; + } else { + // Expand to max size + this.minSize.width = this.maxWidth; + } + + this.paddingTop = this.options.ticks.fontSize / 2; + this.paddingBottom = this.options.ticks.fontSize / 2; + } + } + + if (this.margins) { + this.paddingLeft -= this.margins.left; + this.paddingTop -= this.margins.top; + this.paddingRight -= this.margins.right; + this.paddingBottom -= this.margins.bottom; + + this.paddingLeft = Math.max(this.paddingLeft, 0); + this.paddingTop = Math.max(this.paddingTop, 0); + this.paddingRight = Math.max(this.paddingRight, 0); + this.paddingBottom = Math.max(this.paddingBottom, 0); + } + + this.width = this.minSize.width; + this.height = this.minSize.height; + + }, + afterFit: helpers.noop, + + + + + + + // Shared Methods + isHorizontal: function() { + return this.options.position == "top" || this.options.position == "bottom"; + }, + + // Used to get data value locations. Value can either be an index or a numerical value + getPixelForValue: helpers.noop, + + // Used for tick location, should + getPixelForTick: function(index, includeOffset) { + if (this.isHorizontal()) { + var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + var tickWidth = innerWidth / Math.max((this.ticks.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1); + var pixel = (tickWidth * index) + this.paddingLeft; + + if (includeOffset) { + pixel += tickWidth / 2; + } + return this.left + Math.round(pixel); + } else { + var innerHeight = this.height - (this.paddingTop + this.paddingBottom); + return this.top + (index * (innerHeight / (this.ticks.length - 1))); + } + }, + + // Utility for getting the pixel location of a percentage of scale + getPixelForDecimal: function(decimal, includeOffset) { + if (this.isHorizontal()) { + var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + var valueOffset = (innerWidth * decimal) + this.paddingLeft; + + return this.left + Math.round(valueOffset); + } else { + return this.top + (decimal * (this.height / this.ticks.length)); + } + }, + + // 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 isRotated; + var skipRatio; + var scaleLabelX; + var scaleLabelY; + + // Make sure we draw text in the correct color + this.ctx.fillStyle = this.options.ticks.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; + isRotated = this.labelRotation !== 0; + skipRatio = false; + + if ((this.options.ticks.fontSize + 4) * this.ticks.length > (this.width - (this.paddingLeft + this.paddingRight))) { + skipRatio = 1 + Math.floor(((this.options.ticks.fontSize + 4) * this.ticks.length) / (this.width - (this.paddingLeft + this.paddingRight))); + } + + helpers.each(this.ticks, function(label, index) { + // Blank ticks + if ((skipRatio > 1 && index % skipRatio > 0) || (label === undefined || label === null)) { + return; + } + var xLineValue = this.getPixelForTick(index); // xvalues for grid lines + var xLabelValue = this.getPixelForTick(index, this.options.gridLines.offsetGridLines); // x values for ticks (need to consider offsetLabel option) + + if (this.options.gridLines.show) { + if (index === (typeof this.zeroLineIndex !== 'undefined' ? this.zeroLineIndex : 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.ticks.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); + + if (this.options.scaleLabel.show) { + // Draw the scale label + this.ctx.textAlign = "center"; + this.ctx.textBaseline = 'middle'; + this.ctx.font = helpers.fontString(this.options.scaleLabel.fontSize, this.options.scaleLabel.fontStyle, this.options.scaleLabel.fontFamily); + + scaleLabelX = this.left + ((this.right - this.left) / 2); // midpoint of the width + scaleLabelY = this.options.position == 'bottom' ? this.bottom - (this.options.scaleLabel.fontSize / 2) : this.top + (this.options.scaleLabel.fontSize / 2); + + this.ctx.fillText(this.options.scaleLabel.labelString, scaleLabelX, scaleLabelY); + } + + } else { + setContextLineSettings = true; + var xTickStart = this.options.position == "left" ? this.right : this.left - 10; + var xTickEnd = this.options.position == "left" ? this.right + 10 : this.left; + isRotated = this.labelRotation !== 0; + //skipRatio = false; + + // if ((this.options.ticks.fontSize + 4) * this.ticks.length > (this.width - (this.paddingLeft + this.paddingRight))) { + // skipRatio = 1 + Math.floor(((this.options.ticks.fontSize + 4) * this.ticks.length) / (this.width - (this.paddingLeft + this.paddingRight))); + // } + + helpers.each(this.ticks, function(label, index) { + // Blank ticks + // if ((skipRatio > 1 && index % skipRatio > 0) || (label === undefined || label === null)) { + // return; + // } + var yLineValue = this.getPixelForTick(index); // xvalues for grid lines + var yLabelValue = this.getPixelForTick(index, this.options.gridLines.offsetGridLines); // x values for ticks (need to consider offsetLabel option) + var xLabelValue = this.left + (this.width / 2); + + if (this.options.gridLines.show) { + if (index === (typeof this.zeroLineIndex !== 'undefined' ? this.zeroLineIndex : 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; + } + + yLineValue += helpers.aliasPixel(this.ctx.lineWidth); + + // Draw the label area + this.ctx.beginPath(); + + if (this.options.gridLines.drawTicks) { + this.ctx.moveTo(xTickStart, yLineValue); + this.ctx.lineTo(xTickEnd, yLineValue); + } + + // Draw the chart area + if (this.options.gridLines.drawOnChartArea) { + this.ctx.moveTo(chartArea.left, yLineValue); + this.ctx.lineTo(chartArea.right, yLineValue); + } + + // Need to stroke in the loop because we are potentially changing line widths & colours + this.ctx.stroke(); + } + + if (this.options.ticks.show) { + this.ctx.save(); + this.ctx.translate(xLabelValue, yLabelValue); + this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1); + this.ctx.font = this.font; + this.ctx.textAlign = 'center'; + this.ctx.textBaseline = "middle"; + this.ctx.fillText(label, 0, 0); + this.ctx.restore(); + } + }, this); + + if (this.options.scaleLabel.show) { + // Draw the scale label + scaleLabelX = this.options.position == 'left' ? this.left + (this.options.scaleLabel.fontSize / 2) : this.right - (this.options.scaleLabel.fontSize / 2); + scaleLabelY = this.top + ((this.bottom - this.top) / 2); + var rotation = this.options.position == 'left' ? -0.5 * Math.PI : 0.5 * Math.PI; + + this.ctx.save(); + this.ctx.translate(scaleLabelX, scaleLabelY); + this.ctx.rotate(rotation); + this.ctx.textAlign = "center"; + this.ctx.font = helpers.fontString(this.options.scaleLabel.fontSize, this.options.scaleLabel.fontStyle, this.options.scaleLabel.fontFamily); + this.ctx.textBaseline = 'middle'; + this.ctx.fillText(this.options.scaleLabel.labelString, 0, 0); + this.ctx.restore(); + } + } + } + } + }); }).call(this); @@ -1628,7 +2163,7 @@ 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 + // 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 @@ -1636,11 +2171,12 @@ 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; + this.defaults[type] = helpers.extendDeep({}, Chart.defaults.scale, defaults); }, getScaleConstructor: function(type) { return this.constructors.hasOwnProperty(type) ? this.constructors[type] : undefined; @@ -1649,7 +2185,7 @@ return this.defaults.hasOwnProperty(type) ? this.defaults[type] : {}; }, // The interesting function - fitScalesForChart: function(chartInstance, width, height) { + update: function(chartInstance, width, height) { var xPadding = width > 30 ? 5 : 2; var yPadding = height > 30 ? 5 : 2; @@ -1700,7 +2236,6 @@ chartWidth -= (2 * xPadding); chartHeight -= (2 * yPadding); - // Step 2 var verticalScaleWidth = (width - chartWidth) / (leftScales.length + rightScales.length); @@ -1711,7 +2246,7 @@ var minimumScaleSizes = []; var verticalScaleMinSizeFunction = function(scaleInstance) { - var minSize = scaleInstance.fit(verticalScaleWidth, chartHeight); + var minSize = scaleInstance.update(verticalScaleWidth, chartHeight); minimumScaleSizes.push({ horizontal: false, minSize: minSize, @@ -1720,7 +2255,7 @@ }; var horizontalScaleMinSizeFunction = function(scaleInstance) { - var minSize = scaleInstance.fit(chartWidth, horizontalScaleHeight); + var minSize = scaleInstance.update(chartWidth, horizontalScaleHeight); minimumScaleSizes.push({ horizontal: true, minSize: minSize, @@ -1758,7 +2293,7 @@ }); if (wrapper) { - scaleInstance.fit(wrapper.minSize.width, maxChartHeight); + scaleInstance.update(wrapper.minSize.width, maxChartHeight); } }; @@ -1775,7 +2310,7 @@ }; if (wrapper) { - scaleInstance.fit(maxChartWidth, wrapper.minSize.height, scaleMargin); + scaleInstance.update(maxChartWidth, wrapper.minSize.height, scaleMargin); } }; @@ -1820,7 +2355,7 @@ }; if (wrapper) { - scaleInstance.fit(wrapper.minSize.width, maxChartHeight, scaleMargin); + scaleInstance.update(wrapper.minSize.width, maxChartHeight, scaleMargin); } }); @@ -1837,10 +2372,58 @@ }; if (wrapper) { - scaleInstance.fit(wrapper.minSize.width, maxChartHeight, scaleMargin); + scaleInstance.update(wrapper.minSize.width, maxChartHeight, scaleMargin); } }); + // Recalculate because the size of each scale might have changed slightly due to the margins (label rotation for instance) + totalLeftWidth = xPadding; + totalRightWidth = xPadding; + totalTopHeight = yPadding; + totalBottomHeight = yPadding; + + helpers.each(leftScales, function(scaleInstance) { + totalLeftWidth += scaleInstance.width; + }); + + helpers.each(rightScales, function(scaleInstance) { + totalRightWidth += scaleInstance.width; + }); + + helpers.each(topScales, function(scaleInstance) { + totalTopHeight += scaleInstance.height; + }); + helpers.each(bottomScales, function(scaleInstance) { + totalBottomHeight += scaleInstance.height; + }); + + // Figure out if our chart area changed. This would occur if the dataset scale label rotation + // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do + // without calling `fit` again + var newMaxChartHeight = height - totalTopHeight - totalBottomHeight; + var newMaxChartWidth = width - totalLeftWidth - totalRightWidth; + + if (newMaxChartWidth !== maxChartWidth || newMaxChartHeight !== maxChartHeight) { + helpers.each(leftScales, function(scale) { + scale.height = newMaxChartHeight; + }); + + helpers.each(rightScales, function(scale) { + scale.height = newMaxChartHeight; + }); + + helpers.each(topScales, function(scale) { + scale.width = newMaxChartWidth; + }); + + helpers.each(bottomScales, function(scale) { + scale.width = newMaxChartWidth; + }); + + maxChartHeight = newMaxChartHeight; + maxChartWidth = newMaxChartWidth; + } + // Step 7 // Position the scales var left = xPadding; @@ -1888,6 +2471,8 @@ } } }; + + }).call(this); (function() { @@ -2056,7 +2641,11 @@ x: medianPosition.x, y: medianPosition.y, labels: labels, - title: this._data.labels && this._data.labels.length ? this._data.labels[this._active[0]._index] : '', + title: (function() { + return this._data.timeLabels ? this._data.timeLabels[this._active[0]._index] : + (this._data.labels && this._data.labels.length) ? this._data.labels[this._active[0]._index] : + ''; + }).call(this), legendColors: colors, legendBackgroundColor: this._options.tooltips.multiKeyBackground, }); @@ -2250,8 +2839,10 @@ scales: { xAxes: [{ type: "category", - categorySpacing: 10, - spacing: 1, + + // Specific to Bar Controller + categoryPercentage: 0.8, + barPercentage: 0.9, // grid line settings gridLines: { @@ -2293,7 +2884,7 @@ return this.chart.data.datasets[this.index]; }, - getScaleForId: function(scaleID) { + getScaleForID: function(scaleID) { return this.chart.scales[scaleID]; }, @@ -2335,6 +2926,7 @@ this.updateElement(rectangle, index, true, numBars); this.getDataset().metaData.splice(index, 0, rectangle); }, + removeElement: function(index) { this.getDataset().metaData.splice(index, 1); }, @@ -2343,22 +2935,24 @@ this.update(true); }, - update: function(reset) { - var numBars = this.getBarCount(); - + buildOrUpdateElements: function buildOrUpdateElements() { var numData = this.getDataset().data.length; var numRectangles = this.getDataset().metaData.length; // Make sure that we handle number of datapoints changing if (numData < numRectangles) { // Remove excess bars for data points that have been removed - this.getDataset().metaData.splice(numData, numRectangles - numData) + this.getDataset().metaData.splice(numData, numRectangles - numData); } else if (numData > numRectangles) { // Add new elements for (var index = numRectangles; index < numData; ++index) { this.addElementAndReset(index); } } + }, + + update: function update(reset) { + var numBars = this.getBarCount(); helpers.each(this.getDataset().metaData, function(rectangle, index) { this.updateElement(rectangle, index, reset, numBars); @@ -2366,8 +2960,10 @@ }, updateElement: function updateElement(rectangle, index, reset, numBars) { - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var yScale = this.getScaleForId(this.getDataset().yAxisID); + + var xScale = this.getScaleForID(this.getDataset().xAxisID); + var yScale = this.getScaleForID(this.getDataset().yAxisID); + var yScalePoint; if (yScale.min < 0 && yScale.max < 0) { @@ -2390,16 +2986,16 @@ // Desired view properties _model: { - x: xScale.calculateBarX(numBars, this.index, index), - y: reset ? yScalePoint : yScale.calculateBarY(this.index, index), + x: this.calculateBarX(index, this.index), + y: reset ? yScalePoint : this.calculateBarY(index, this.index), // Tooltip label: this.chart.data.labels[index], datasetLabel: this.getDataset().label, // Appearance - base: yScale.calculateBarBase(this.index, index), - width: xScale.calculateBarWidth(numBars), + base: this.calculateBarBase(this.index, index), + width: this.calculateBarWidth(numBars), 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), @@ -2408,6 +3004,147 @@ rectangle.pivot(); }, + calculateBarBase: function(datasetIndex, index) { + + var xScale = this.getScaleForID(this.getDataset().xAxisID); + var yScale = this.getScaleForID(this.getDataset().yAxisID); + + var base = 0; + + if (yScale.options.stacked) { + + var value = this.chart.data.datasets[datasetIndex].data[index]; + + if (value < 0) { + for (var i = 0; i < datasetIndex; i++) { + if (this.chart.data.datasets[i].yAxisID === yScale.id) { + base += this.chart.data.datasets[i].data[index] < 0 ? this.chart.data.datasets[i].data[index] : 0; + } + } + } else { + for (var j = 0; j < datasetIndex; j++) { + if (this.chart.data.datasets[j].yAxisID === yScale.id) { + base += this.chart.data.datasets[j].data[index] > 0 ? this.chart.data.datasets[j].data[index] : 0; + } + } + } + + return yScale.getPixelForValue(base); + } + + base = yScale.getPixelForValue(yScale.min); + + if (yScale.beginAtZero || ((yScale.min <= 0 && yScale.max >= 0) || (yScale.min >= 0 && yScale.max <= 0))) { + base = yScale.getPixelForValue(0, 0); + //base += yScale.options.gridLines.lineWidth; + } else if (yScale.min < 0 && yScale.max < 0) { + // All values are negative. Use the top as the base + base = yScale.getPixelForValue(yScale.max); + } + + return base; + + }, + + getRuler: function() { + + var xScale = this.getScaleForID(this.getDataset().xAxisID); + var yScale = this.getScaleForID(this.getDataset().yAxisID); + + var datasetCount = !this.chart.isCombo ? this.chart.data.datasets.length : helpers.where(this.chart.data.datasets, function(ds) { + return ds.type == 'bar'; + }).length; + var tickWidth = (function() { + var min = xScale.getPixelForValue(null, 1) - xScale.getPixelForValue(null, 0); + for (var i = 2; i < this.getDataset().data.length; i++) { + min = Math.min(xScale.getPixelForValue(null, i) - xScale.getPixelForValue(null, i - 1), min); + } + return min; + }).call(this); + var categoryWidth = tickWidth * xScale.options.categoryPercentage; + var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2; + var fullBarWidth = categoryWidth / datasetCount; + var barWidth = fullBarWidth * xScale.options.barPercentage; + var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage); + + return { + datasetCount: datasetCount, + tickWidth: tickWidth, + categoryWidth: categoryWidth, + categorySpacing: categorySpacing, + fullBarWidth: fullBarWidth, + barWidth: barWidth, + barSpacing: barSpacing, + }; + }, + + calculateBarWidth: function() { + + var xScale = this.getScaleForID(this.getDataset().xAxisID); + var ruler = this.getRuler(); + + if (xScale.options.stacked) { + return ruler.categoryWidth; + } + + return ruler.barWidth; + + }, + + + calculateBarX: function(index, datasetIndex) { + + var yScale = this.getScaleForID(this.getDataset().yAxisID); + var xScale = this.getScaleForID(this.getDataset().xAxisID); + + var ruler = this.getRuler(); + var leftTick = xScale.getPixelForValue(null, index, datasetIndex); + leftTick -= this.chart.isCombo ? (ruler.tickWidth / 2) : 0; + + if (yScale.options.stacked) { + return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing; + } + + return leftTick + + (ruler.barWidth / 2) + + ruler.categorySpacing + + (ruler.barWidth * datasetIndex) + + (ruler.barSpacing / 2) + + (ruler.barSpacing * datasetIndex); + }, + + calculateBarY: function(index, datasetIndex) { + + var xScale = this.getScaleForID(this.getDataset().xAxisID); + var yScale = this.getScaleForID(this.getDataset().yAxisID); + + var value = this.getDataset().data[index]; + + if (yScale.options.stacked) { + + var sumPos = 0, + sumNeg = 0; + + for (var i = 0; i < datasetIndex; i++) { + if (this.chart.data.datasets[i].data[index] < 0) { + sumNeg += this.chart.data.datasets[i].data[index] || 0; + } else { + sumPos += this.chart.data.datasets[i].data[index] || 0; + } + } + + if (value < 0) { + return yScale.getPixelForValue(sumNeg + value); + } else { + return yScale.getPixelForValue(sumPos + value); + } + + return yScale.getPixelForValue(value); + } + + return yScale.getPixelForValue(value); + }, + draw: function(ease) { var easingDecimal = ease || 1; helpers.each(this.getDataset().metaData, function(rectangle, index) { @@ -2525,20 +3262,7 @@ this.update(true); }, - update: function(reset) { - - this.chart.outerRadius = (helpers.min([this.chart.chart.width, this.chart.chart.height]) / 2) - this.chart.options.elements.arc.borderWidth / 2; - this.chart.innerRadius = this.chart.options.cutoutPercentage ? (this.chart.outerRadius / 100) * (this.chart.options.cutoutPercentage) : 1; - this.chart.radiusLength = (this.chart.outerRadius - this.chart.innerRadius) / this.chart.data.datasets.length; - - this.getDataset().total = 0; - helpers.each(this.getDataset().data, function(value) { - this.getDataset().total += Math.abs(value); - }, this); - - this.outerRadius = this.chart.outerRadius - (this.chart.radiusLength * this.index); - this.innerRadius = this.outerRadius - this.chart.radiusLength; - + buildOrUpdateElements: function buildOrUpdateElements() { // Make sure we have metaData for each data point var numData = this.getDataset().data.length; var numArcs = this.getDataset().metaData.length; @@ -2553,6 +3277,21 @@ this.addElementAndReset(index); } } + }, + + update: function update(reset) { + + this.chart.outerRadius = Math.max((helpers.min([this.chart.chart.width, this.chart.chart.height]) / 2) - this.chart.options.elements.arc.borderWidth / 2, 0); + this.chart.innerRadius = Math.max(this.chart.options.cutoutPercentage ? (this.chart.outerRadius / 100) * (this.chart.options.cutoutPercentage) : 1, 0); + this.chart.radiusLength = (this.chart.outerRadius - this.chart.innerRadius) / this.chart.data.datasets.length; + + this.getDataset().total = 0; + helpers.each(this.getDataset().data, function(value) { + this.getDataset().total += Math.abs(value); + }, this); + + this.outerRadius = this.chart.outerRadius - (this.chart.radiusLength * this.index); + this.innerRadius = this.outerRadius - this.chart.radiusLength; helpers.each(this.getDataset().metaData, function(arc, index) { this.updateElement(arc, index, reset); @@ -2752,14 +3491,7 @@ this.update(true); }, - update: function(reset) { - var line = this.getDataset().metaDataset; - var points = this.getDataset().metaData; - - var yScale = this.getScaleForId(this.getDataset().yAxisID); - var xScale = this.getScaleForId(this.getDataset().xAxisID); - var scaleBase; - + buildOrUpdateElements: function buildOrUpdateElements() { // Handle the number of data points changing var numData = this.getDataset().data.length; var numPoints = this.getDataset().metaData.length; @@ -2767,13 +3499,22 @@ // Make sure that we handle number of datapoints changing if (numData < numPoints) { // Remove excess bars for data points that have been removed - this.getDataset().metaData.splice(numData, numPoints - numData) + this.getDataset().metaData.splice(numData, numPoints - numData); } else if (numData > numPoints) { // Add new elements for (var index = numPoints; index < numData; ++index) { this.addElementAndReset(index); } } + }, + + update: function update(reset) { + var line = this.getDataset().metaDataset; + var points = this.getDataset().metaData; + + var yScale = this.getScaleForId(this.getDataset().yAxisID); + var xScale = this.getScaleForId(this.getDataset().xAxisID); + var scaleBase; if (yScale.min < 0 && yScale.max < 0) { scaleBase = yScale.getPixelForValue(yScale.max); @@ -2843,8 +3584,8 @@ // Desired view properties _model: { - x: xScale.getPointPixelForValue(this.getDataset().data[index], index, this.index), - y: reset ? scaleBase : yScale.getPointPixelForValue(this.getDataset().data[index], index, this.index), + x: xScale.getPixelForValue(this.getDataset().data[index], index, this.index, this.chart.isCombo), + y: reset ? scaleBase : yScale.getPixelForValue(this.getDataset().data[index], index, this.index), // Appearance tension: point.custom && point.custom.tension ? point.custom.tension : (this.getDataset().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), @@ -3018,16 +3759,33 @@ this.update(true); }, - update: function(reset) { + buildOrUpdateElements: function buildOrUpdateElements() { + // Handle the number of data points changing + var numData = this.getDataset().data.length; + var numPoints = this.getDataset().metaData.length; - Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height); + // Make sure that we handle number of datapoints changing + if (numData < numPoints) { + // Remove excess bars for data points that have been removed + this.getDataset().metaData.splice(numData, numPoints - numData) + } else if (numData > numPoints) { + // Add new elements + for (var index = numPoints; index < numData; ++index) { + this.addElementAndReset(index); + } + } + }, + + update: function update(reset) { + + Chart.scaleService.update(this, this.chart.width, this.chart.height); //this.chart.scale.setScaleSize(); this.chart.scale.calculateRange(); this.chart.scale.generateTicks(); this.chart.scale.buildYLabels(); - this.chart.outerRadius = (helpers.min([this.chart.chart.width, this.chart.chart.height]) - this.chart.options.elements.arc.borderWidth / 2) / 2; - this.chart.innerRadius = this.chart.options.cutoutPercentage ? (this.chart.outerRadius / 100) * (this.chart.options.cutoutPercentage) : 1; + this.chart.outerRadius = Math.max((helpers.min([this.chart.chart.width, this.chart.chart.height]) - this.chart.options.elements.arc.borderWidth / 2) / 2, 0); + this.chart.innerRadius = Math.max(this.chart.options.cutoutPercentage ? (this.chart.outerRadius / 100) * (this.chart.options.cutoutPercentage) : 1, 0); this.chart.radiusLength = (this.chart.outerRadius - this.chart.innerRadius) / this.chart.data.datasets.length; this.getDataset().total = 0; @@ -3238,7 +3996,24 @@ this.update(true); }, - update: function(reset) { + buildOrUpdateElements: function buildOrUpdateElements() { + // Handle the number of data points changing + var numData = this.getDataset().data.length; + var numPoints = this.getDataset().metaData.length; + + // Make sure that we handle number of datapoints changing + if (numData < numPoints) { + // Remove excess bars for data points that have been removed + this.getDataset().metaData.splice(numData, numPoints - numData) + } else if (numData > numPoints) { + // Add new elements + for (var index = numPoints; index < numData; ++index) { + this.addElementAndReset(index); + } + } + }, + + update: function update(reset) { var line = this.getDataset().metaDataset; var points = this.getDataset().metaData; @@ -3409,55 +4184,20 @@ // Default config for a category scale var defaultConfig = { - display: true, position: "bottom", - - // 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, - }, - - // label settings - labels: { - show: true, - maxRotation: 90, - template: "<%=value%>", - fontSize: 12, - fontStyle: "normal", - fontColor: "#666", - fontFamily: "Helvetica Neue", - }, }; - var DatasetScale = Chart.Element.extend({ - isHorizontal: function() { - return this.options.position == "top" || this.options.position == "bottom"; + var DatasetScale = Chart.Scale.extend({ + buildTicks: function(index) { + this.ticks = this.data.labels; }, - buildLabels: function(index) { - this.labels = []; - if (this.options.labels.userCallback) { - this.data.labels.forEach(function(labelString, index) { - this.labels.push(this.options.labels.userCallback(labelString, index)); - }, this); - } else { - this.labels = this.data.labels; - } - }, + // Used to get data value locations. Value can either be an index or a numerical value 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.labels.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1); + var valueWidth = innerWidth / Math.max((this.data.labels.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1); var valueOffset = (valueWidth * index) + this.paddingLeft; if (this.options.gridLines.offsetGridLines && includeOffset) { @@ -3469,231 +4209,10 @@ return this.top + (index * (this.height / this.labels.length)); } }, - getPointPixelForValue: function(value, index, datasetIndex) { - return this.getPixelForValue(value, index, datasetIndex, true); - }, - - // 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(barDatasetCount) { - //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset - var baseWidth = this.calculateBaseWidth() - ((barDatasetCount - 1) * this.options.spacing); - - if (this.options.stacked) { - return baseWidth; - } - return (baseWidth / barDatasetCount); - }, - calculateBarX: function(barDatasetCount, datasetIndex, elementIndex) { - var xWidth = this.calculateBaseWidth(), - xAbsolute = this.getPixelForValue(null, elementIndex, datasetIndex, true) - (xWidth / 2), - barWidth = this.calculateBarWidth(barDatasetCount); - - if (this.options.stacked) { - return xAbsolute + barWidth / 2; - } - - return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * this.options.spacing) + barWidth / 2; - }, - - 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; - - 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; - - this.paddingRight = lastWidth / 2 + 3; - this.paddingLeft = firstWidth / 2 + 3; - - this.labelRotation = 0; - - if (this.options.display) { - var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels); - var cosRotation; - var sinRotation; - var firstRotatedWidth; - - 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 datasetWidth = Math.floor(this.getPixelForValue(0, 1) - this.getPixelForValue(0, 0)) - 6; - - //Max label rotation can be set or default to 90 - also act as a loop counter - while (this.labelWidth > datasetWidth && this.labelRotation <= this.options.labels.maxRotation) { - 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) { - // Set the unconstrained dimension before label rotation - if (this.isHorizontal()) { - this.width = maxWidth; - } else { - this.height = maxHeight; - } - - this.buildLabels(); - 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.labels); - - // Width - if (this.isHorizontal()) { - minSize.width = maxWidth; - } else if (this.options.display) { - var labelWidth = this.options.labels.show ? longestLabelWidth + 6 : 0; - minSize.width = Math.min(labelWidth, maxWidth); - } - - // Height - if (this.isHorizontal() && this.options.display) { - var labelHeight = (Math.sin(helpers.toRadians(this.labelRotation)) * longestLabelWidth) + 1.5 * this.options.labels.fontSize; - minSize.height = Math.min(this.options.labels.show ? labelHeight : 0, maxHeight); - } else if (this.options.display) { - minSize.height = maxHeight; - } - - 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; - var skipRatio = false; - - if ((this.options.labels.fontSize + 4) * this.labels.length > (this.width - (this.paddingLeft + this.paddingRight))) { - skipRatio = 1 + Math.floor(((this.options.labels.fontSize + 4) * this.labels.length) / (this.width - (this.paddingLeft + this.paddingRight))); - } - - helpers.each(this.labels, function(label, index) { - // Blank labels - if ((skipRatio > 1 && index % skipRatio > 0) || (label === undefined || label === null)) { - return; - } - 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() { @@ -3704,180 +4223,13 @@ helpers = Chart.helpers; var defaultConfig = { - display: true, position: "left", - - // 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 - reverse: false, - beginAtZero: false, - override: null, - - // label settings - labels: { - show: true, - mirror: false, - padding: 10, - 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 + var LinearScale = Chart.Scale.extend({ + buildTicks: function() { - // 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); - this.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); - - if (this.options.reverse) { - this.ticks.reverse(); - - this.start = this.max; - this.end = this.min; - } else { - this.start = this.min; - this.end = this.max; - } - }, - 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" && rawValue !== null) ? (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.end - this.start; - - if (this.isHorizontal()) { - var innerWidth = this.width - (this.paddingLeft + this.paddingRight); - pixel = this.left + (innerWidth / range * (value - this.start)); - pixel += this.paddingLeft; - } else { - // Bottom - top since pixels increase downard on a screen - var innerHeight = this.height - (this.paddingTop + this.paddingBottom); - pixel = (this.bottom - this.paddingBottom) - (innerHeight / range * (value - this.start)); - } - - return pixel; - }, - - // Functions needed for line charts - calculateRange: function() { + // First Calculate the range this.min = null; this.max = null; @@ -3937,374 +4289,299 @@ this.min--; this.max++; } - }, - - getPointPixelForValue: function(rawValue, index, datasetIndex) { - var value = this.getRightValue(rawValue); - - if (this.options.stacked) { - var offsetPos = 0; - var offsetNeg = 0; - - for (var i = this.data.datasets.length - 1; 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); - } - - 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, margins) { - 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 - // | - // -| - // | - // | - // -| - // | - // | - // -| - // Width + // Then calulate the ticks + this.ticks = []; + + // 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()) { - minSize.width = maxWidth; // fill all the width + maxTicks = Math.min(11, Math.ceil(this.width / 50)); } else { - minSize.width = this.options.gridLines.show && this.options.display ? 10 : 0; + // The factor of 2 used to scale the font size has been experimentally determined. + maxTicks = Math.min(11, Math.ceil(this.height / (2 * this.options.ticks.fontSize))); } - // height - if (this.isHorizontal()) { - minSize.height = this.options.gridLines.show && this.options.display ? 10 : 0; - } else { - minSize.height = maxHeight; // fill all the height - } + // Make sure we always have at least 2 ticks + maxTicks = Math.max(2, maxTicks); - this.paddingLeft = 0; - this.paddingRight = 0; - this.paddingTop = 0; - this.paddingBottom = 0; + // 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 (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); - - var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); - this.ctx.font = labelFont; - - var firstLabelWidth = this.ctx.measureText(this.labels[0]).width; - var lastLabelWidth = this.ctx.measureText(this.labels[this.labels.length - 1]).width; - - // Ensure that our labels are always inside the canvas - this.paddingLeft = firstLabelWidth / 2; - this.paddingRight = lastLabelWidth / 2; - } 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; - minSize.width += 3; // extra padding - } else { - // Expand to max size - minSize.width = maxWidth; - } - - this.paddingTop = this.options.labels.fontSize / 2; - this.paddingBottom = this.options.labels.fontSize / 2; + 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 (margins) { - this.paddingLeft -= margins.left; - this.paddingTop -= margins.top; - this.paddingRight -= margins.right; - this.paddingBottom -= margins.bottom; + 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.paddingLeft = Math.max(this.paddingLeft, 0); - this.paddingTop = Math.max(this.paddingTop, 0); - this.paddingRight = Math.max(this.paddingRight, 0); - this.paddingBottom = Math.max(this.paddingBottom, 0); + // Put the values into the ticks array + for (var j = niceMin; j <= niceMax; j += spacing) { + this.ticks.push(j); } - this.width = minSize.width; - this.height = minSize.height; - return minSize; + 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); + + if (this.options.reverse) { + this.ticks.reverse(); + + this.start = this.max; + this.end = this.min; + } else { + this.start = this.max; + this.end = this.min; + } + + this.zeroLineIndex = this.ticks.indexOf(0); }, - // 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; - 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; + // Utils - helpers.each(this.ticks, function(tick, index) { - // Grid lines are vertical - var xValue = this.getPixelForValue(tick); + 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 + var pixel; + var range = this.end - this.start; - 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; - } + if (this.isHorizontal()) { - xValue += helpers.aliasPixel(this.ctx.lineWidth); - - // Draw the label area - this.ctx.beginPath(); - - if (this.options.gridLines.drawTicks) { - this.ctx.moveTo(xValue, yTickStart); - this.ctx.lineTo(xValue, yTickEnd); - } - - // Draw the chart area - if (this.options.gridLines.drawOnChartArea) { - this.ctx.moveTo(xValue, chartArea.top); - this.ctx.lineTo(xValue, chartArea.bottom); - } - - // Need to stroke in the loop because we are potentially changing line widths & colours - this.ctx.stroke(); - }, this); - } - - if (this.options.labels.show) { - // Draw the labels - - var labelStartY; - - 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.ctx.textAlign = "center"; - 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 xValue = this.getPixelForValue(this.ticks[index]); - this.ctx.fillText(label, xValue, labelStartY); - }, this); - } - } else { - // Vertical - if (this.options.gridLines.show) { - - // 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); - - 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") { - if (this.options.labels.mirror) { - labelStartX = this.right + this.options.labels.padding; - this.ctx.textAlign = "left"; - } else { - labelStartX = this.right - this.options.labels.padding; - this.ctx.textAlign = "right"; - } - } else { - // right side - if (this.options.labels.mirror) { - labelStartX = this.left - this.options.labels.padding; - this.ctx.textAlign = "right"; - } else { - labelStartX = this.left + this.options.labels.padding; - 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); - } - } + var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + pixel = this.left + (innerWidth / range * (this.getRightValue(value) - this.start)); + return Math.round(pixel + this.paddingLeft); + } else { + var innerHeight = this.height - (this.paddingTop + this.paddingBottom); + pixel = this.top + (innerHeight / range * (this.getRightValue(value) - this.start)); + return Math.round(pixel + this.paddingTop); } - } + }, + + // 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" && rawValue !== null) ? (this.isHorizontal() ? rawValue.x : rawValue.y) : rawValue; + }, + }); Chart.scaleService.registerScaleType("linear", LinearScale, defaultConfig); }).call(this); +(function() { + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + position: "left", + + // scale label + scaleLabel: { + // actual label + labelString: '', + // display property + show: false, + }, + + // label settings + labels: { + template: "<%var remain = value / (Math.pow(10, Math.floor(Chart.helpers.log10(value))));if (remain === 1 || remain === 2 || remain === 5) {%><%=value.toExponential()%><%} else {%><%= null %><%}%>", + } + }; + + var LogarithmicScale = Chart.Scale.extend({ + buildTicks: function() { + + // Calculate Range (we may break this out into it's own lifecycle function) + + this.min = null; + this.max = null; + + var values = []; + + 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); + + values[index] = values[index] || 0; + + if (this.options.relativePoints) { + values[index] = 100; + } else { + // Don't need to split positive and negative since the log scale can't handle a 0 crossing + values[index] += value; + } + }, this); + } + }, this); + + 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); + } + + if (this.min === this.max) { + if (this.min !== 0 && this.min !== null) { + this.min = Math.pow(10, Math.floor(helpers.log10(this.min)) - 1); + this.max = Math.pow(10, Math.floor(helpers.log10(this.max)) + 1); + } else { + this.min = 1; + this.max = 10; + } + } + + + // 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.tickValues = []; + + // 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 minExponent = Math.floor(helpers.log10(this.min)); + var maxExponent = Math.ceil(helpers.log10(this.max)); + + for (var exponent = minExponent; exponent < maxExponent; ++exponent) { + for (var i = 1; i < 10; ++i) { + this.tickValues.push(i * Math.pow(10, exponent)); + } + } + + this.tickValues.push(1.0 * Math.pow(10, maxExponent)); + + 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.tickValues.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.tickValues); + this.min = helpers.min(this.tickValues); + + if (this.options.reverse) { + this.tickValues.reverse(); + + this.start = this.max; + this.end = this.min; + } else { + this.start = this.min; + this.end = this.max; + } + + this.ticks = []; + + helpers.each(this.tickValues, 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.ticks.push(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; + }, + getPixelForTick: function(index, includeOffset) { + return this.getPixelForValue(this.tickValues[index], null, null, includeOffset); + }, + getPixelForValue: function(value, index, datasetIndex, includeOffset) { + var pixel; + + var newVal = this.getRightValue(value); + var range = helpers.log10(this.end) - helpers.log10(this.start); + + if (this.isHorizontal()) { + + if (newVal === 0) { + pixel = this.left + this.paddingLeft; + } else { + var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + pixel = this.left + (innerWidth / range * (helpers.log10(newVal) - helpers.log10(this.start))); + return pixel + this.paddingLeft; + } + } else { + // Bottom - top since pixels increase downard on a screen + if (newVal === 0) { + pixel = this.top + this.paddingTop; + } else { + var innerHeight = this.height - (this.paddingTop + this.paddingBottom); + return (this.bottom - this.paddingBottom) - (innerHeight / range * (helpers.log10(newVal) - helpers.log10(this.start))); + } + } + + }, + + }); + Chart.scaleService.registerScaleType("logarithmic", LogarithmicScale, defaultConfig); + +}).call(this); + (function() { "use strict"; @@ -4334,6 +4611,7 @@ }, // scale numbers + reverse: false, beginAtZero: true, // label settings @@ -4401,6 +4679,8 @@ helpers.each(this.data.datasets, function(dataset) { helpers.each(dataset.data, function(value, index) { + if (value === null) return; + if (this.min === null) { this.min = value; } else if (value < this.min) { @@ -4634,9 +4914,14 @@ return index * angleMultiplier - (Math.PI / 2); }, getDistanceFromCenterForValue: function(value) { + if (value === null) return 0; // null always in center // 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; + if (this.options.reverse) { + return (this.max - value) * scalingFactor; + } else { + return (value - this.min) * scalingFactor; + } }, getPointPosition: function(index, distanceFromCenter) { var thisAngle = this.getIndexAngle(index); @@ -4652,8 +4937,8 @@ if (this.options.display) { var ctx = this.ctx; helpers.each(this.yLabels, function(label, index) { - // Don't draw a centre value - if (index > 0) { + // Don't draw a centre value (if it is minimum) + if (index > 0 || this.options.reverse) { var yCenterOffset = this.getDistanceFromCenterForValue(this.ticks[index]); var yHeight = this.yCenter - yCenterOffset; @@ -4712,7 +4997,7 @@ for (var i = this.getValueCount() - 1; i >= 0; i--) { if (this.options.angleLines.show) { - var outerPosition = this.getPointPosition(i, this.getDistanceFromCenterForValue(this.max)); + var outerPosition = this.getPointPosition(i, this.getDistanceFromCenterForValue(this.options.reverse ? this.min : this.max)); ctx.beginPath(); ctx.moveTo(this.xCenter, this.yCenter); ctx.lineTo(outerPosition.x, outerPosition.y); @@ -4720,7 +5005,7 @@ ctx.closePath(); } // Extra 3px out for some label spacing - var pointLabelPosition = this.getPointPosition(i, this.getDistanceFromCenterForValue(this.max) + 5); + var pointLabelPosition = this.getPointPosition(i, this.getDistanceFromCenterForValue(this.options.reverse ? this.min : 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; @@ -4759,6 +5044,192 @@ }).call(this); +(function() { + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var time = { + units: [ + 'millisecond', + 'second', + 'minute', + 'hour', + 'day', + 'week', + 'month', + 'quarter', + 'year', + ], + unit: { + 'millisecond': { + display: 'SSS [ms]', // 002 ms + maxStep: 1000, + }, + 'second': { + display: 'h:mm:ss a', // 11:20:01 AM + maxStep: 60, + }, + 'minute': { + display: 'h:mm:ss a', // 11:20:01 AM + maxStep: 60, + }, + 'hour': { + display: 'MMM D, hA', // Sept 4, 5PM + maxStep: 24, + }, + 'day': { + display: 'll', // Sep 4 2015 + maxStep: 7, + }, + 'week': { + display: 'll', // Week 46, or maybe "[W]WW - YYYY" ? + maxStep: 4.3333, + }, + 'month': { + display: 'MMM YYYY', // Sept 2015 + maxStep: 12, + }, + 'quarter': { + display: '[Q]Q - YYYY', // Q3 + maxStep: 4, + }, + 'year': { + display: 'YYYY', // 2015 + maxStep: false, + }, + } + }; + + var defaultConfig = { + position: "bottom", + + time: { + format: false, // false == date objects or use pattern string from http://momentjs.com/docs/#/parsing/string-format/ + unit: false, // false == automatic or override with week, month, year, etc. + round: false, // none, or override with week, month, year, etc. + displayFormat: false, // defaults to unit's corresponding unitFormat below or override using pattern string from http://momentjs.com/docs/#/displaying/format/ + }, + }; + + var TimeScale = Chart.Scale.extend({ + buildTicks: function(index) { + + this.ticks = []; + this.labelMoments = []; + + // Parse each label into a moment + this.data.labels.forEach(function(label, index) { + var labelMoment = this.parseTime(label); + if (this.options.time.round) { + labelMoment.startOf(this.options.time.round); + } + this.labelMoments.push(labelMoment); + }, this); + + // Find the first and last moments, and range + this.firstTick = moment.min.call(this, this.labelMoments).clone(); + this.lastTick = moment.max.call(this, this.labelMoments).clone(); + + // Set unit override if applicable + if (this.options.time.unit) { + this.tickUnit = this.options.time.unit || 'day'; + this.displayFormat = time.unit.day.display; + this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, this.tickUnit, true)); + } else { + // Determine the smallest needed unit of the time + var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + var labelCapacity = innerWidth / this.options.ticks.fontSize + 4; + var buffer = this.options.time.round ? 0 : 2; + + this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, true) + buffer); + var done; + + helpers.each(time.units, function(format) { + if (this.tickRange <= labelCapacity) { + return; + } + this.tickUnit = format; + this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, this.tickUnit) + buffer); + this.displayFormat = time.unit[format].display; + + }, this); + } + + this.firstTick.startOf(this.tickUnit); + this.lastTick.endOf(this.tickUnit); + this.smallestLabelSeparation = this.width; + + var i = 0; + + for (i = 1; i < this.labelMoments.length; i++) { + this.smallestLabelSeparation = Math.min(this.smallestLabelSeparation, this.labelMoments[i].diff(this.labelMoments[i - 1], this.tickUnit, true)); + } + + + // Tick displayFormat override + if (this.options.time.displayFormat) { + this.displayFormat = this.options.time.displayFormat; + } + + // For every unit in between the first and last moment, create a moment and add it to the ticks tick + if (this.options.ticks.userCallback) { + for (i = 0; i <= this.tickRange; i++) { + this.ticks.push( + this.options.ticks.userCallback(this.firstTick.clone() + .add(i, this.tickUnit) + .format(this.options.time.displayFormat ? this.options.time.displayFormat : time.unit[this.tickUnit].display) + ) + ); + } + } else { + for (i = 0; i <= this.tickRange; i++) { + this.ticks.push(this.firstTick.clone() + .add(i, this.tickUnit) + .format(this.options.time.displayFormat ? this.options.time.displayFormat : time.unit[this.tickUnit].display) + ); + } + } + }, + getPixelForValue: function(value, index, datasetIndex, includeOffset) { + + var offset = this.labelMoments[index].diff(this.firstTick, this.tickUnit, true); + + var decimal = offset / this.tickRange; + + if (this.isHorizontal()) { + var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + var valueWidth = innerWidth / Math.max(this.ticks.length - 1, 1); + var valueOffset = (innerWidth * decimal) + this.paddingLeft; + + return this.left + Math.round(valueOffset); + } else { + return this.top + (decimal * (this.height / this.ticks.length)); + } + }, + parseTime: function(label) { + // Date objects + if (typeof label.getMonth === 'function' || typeof label == 'number') { + return moment(label); + } + // Moment support + if (label.isValid && label.isValid()) { + return label; + } + // Custom parsing (return an instance of moment) + if (typeof this.options.time.format !== 'string' && this.options.time.format.call) { + return this.options.time.format(label); + } + // Moment format parsing + return moment(label, this.options.time.format); + }, + }); + Chart.scaleService.registerScaleType("time", TimeScale, defaultConfig); + +}).call(this); + /*! * Chart.js * http://chartjs.org/ @@ -5203,7 +5674,7 @@ } else { inRange = (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.base && mouseY <= vm.y); } - } + } return inRange; }, @@ -5247,8 +5718,8 @@ config.type = 'bar'; return new Chart(context, config); - } - + }; + }).call(this); (function() { @@ -5259,6 +5730,7 @@ var helpers = Chart.helpers; var defaultConfig = { + aspectRatio: 1, legendTemplate: "
m&&(m=t.x+a,o=e):e>this.getValueCount()/2&&t.x-a
0){var s=this.getDistanceFromCenterForValue(this.ticks[a]),o=this.yCenter-s;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,s,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var n=0;n<% for (var i = 0; i < data.datasets.length; i++){%>
'}},"undefined"!=typeof amd?define(function(){return i}):"object"==typeof module&&module.exports&&(module.exports=i),t.Chart=i,i.noConflict=function(){return t.Chart=e,i}}).call(this),function(){"use strict";var t=this,e=(t.Chart,Chart.helpers={}),i=e.each=function(t,e,i,a){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;if(a)for(n=t.length-1;n>=0;n--)e.apply(i,[t[n],n].concat(s));else for(n=0;n<% for (var i = 0; i < data.datasets[0].data.length; i++){%>
'};e.PolarArea=function(t,s){return s.options=i.configMerge(a,s.options),s.type="polarArea",new e(t,s)}}.call(this),function(){"use strict";{var t=this,e=t.Chart;e.helpers}e.Radar=function(t,i){return i.type="radar",new e(t,i)}}.call(this),function(){"use strict";var t=this,e=t.Chart,i=e.helpers,a={hover:{mode:"single"},scales:{xAxes:[{type:"linear",position:"bottom",id:"x-axis-1"}],yAxes:[{type:"linear",position:"left",id:"y-axis-1"}]},tooltips:{template:"(<%= value.x %>, <%= value.y %>)",multiTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%>(<%= value.x %>, <%= value.y %>)"}};e.Scatter=function(t,s){return s.options=i.configMerge(a,s.options),s.type="line",new e(t,s)}}.call(this),!function t(e,i,a){function s(n,r){if(!i[n]){if(!e[n]){var h="function"==typeof require&&require;if(!r&&h)return h(n,!0);if(o)return o(n,!0);var l=new Error("Cannot find module '"+n+"'");throw l.code="MODULE_NOT_FOUND",l}var c=i[n]={exports:{}};e[n][0].call(c.exports,function(t){var i=e[n][1][t];return s(i?i:t)},c,c.exports,t,e,i,a)}return i[n].exports}for(var o="function"==typeof require&&require,n=0;n<% for (var i = 0; i < data.datasets[0].data.length; i++){%>
'};e.PolarArea=function(t,s){return s.options=i.configMerge(a,s.options),s.type="polarArea",new e(t,s)}}.call(this),function(){"use strict";var t=this,e=t.Chart,i=e.helpers,a={aspectRatio:1};e.Radar=function(t,s){return s.options=i.configMerge(a,s.options),s.type="radar",new e(t,s)}}.call(this),function(){"use strict";var t=this,e=t.Chart,i=e.helpers,a={hover:{mode:"single"},scales:{xAxes:[{type:"linear",position:"bottom",id:"x-axis-1"}],yAxes:[{type:"linear",position:"left",id:"y-axis-1"}]},tooltips:{template:"(<%= value.x %>, <%= value.y %>)",multiTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%>(<%= value.x %>, <%= value.y %>)"}};e.Scatter=function(t,s){return s.options=i.configMerge(a,s.options),s.type="line",new e(t,s)}}.call(this),!function t(e,i,a){function s(o,r){if(!i[o]){if(!e[o]){var h="function"==typeof require&&require;if(!r&&h)return h(o,!0);if(n)return n(o,!0);var l=new Error("Cannot find module '"+o+"'");throw l.code="MODULE_NOT_FOUND",l}var c=i[o]={exports:{}};e[o][0].call(c.exports,function(t){var i=e[o][1][t];return s(i?i:t)},c,c.exports,t,e,i,a)}return i[o].exports}for(var n="function"==typeof require&&require,o=0;o
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/line-time-scale.html b/samples/line-time-scale.html
index 7e238dcdf..d25cb8890 100644
--- a/samples/line-time-scale.html
+++ b/samples/line-time-scale.html
@@ -144,7 +144,8 @@
newDataset.data.push(randomScalingFactor());
}
- window.myLine.addDataset(newDataset);
+ config.data.datasets.push(newDataset);
+ window.myLine.update();
updateLegend();
});
@@ -158,15 +159,17 @@
);
for (var index = 0; index < config.data.datasets.length; ++index) {
- window.myLine.addData(randomScalingFactor(), index);
+ config.data.datasets[index].data.push(randomScalingFactor());
}
+ window.myLine.update();
updateLegend();
}
});
$('#removeDataset').click(function() {
- window.myLine.removeDataset(0);
+ config.data.datasets.splice(0, 1);
+ window.myLine.update();
updateLegend();
});
@@ -174,9 +177,10 @@
config.data.labels.splice(-1, 1); // remove the label first
config.data.datasets.forEach(function(dataset, datasetIndex) {
- window.myLine.removeData(datasetIndex, -1);
+ dataset.data.pop();
});
+ window.myLine.update();
updateLegend();
});
diff --git a/samples/line-x-axis-filter.html b/samples/line-x-axis-filter.html
index 39a19ea3d..8135dbafe 100644
--- a/samples/line-x-axis-filter.html
+++ b/samples/line-x-axis-filter.html
@@ -108,7 +108,8 @@
newDataset.data.push(randomScalingFactor());
}
- window.myLine.addDataset(newDataset);
+ config.data.datasets.push(newDataset);
+ window.myLine.update();
});
$('#addData').click(function() {
@@ -116,21 +117,26 @@
config.data.labels.push('dataset #' + config.data.labels.length);
for (var index = 0; index < config.data.datasets.length; ++index) {
- window.myLine.addData(randomScalingFactor(), index);
+ config.data.datasets[index].data.push(randomScalingFactor());
}
+
+ window.myLine.update();
}
});
$('#removeDataset').click(function() {
- window.myLine.removeDataset(0);
+ config.data.datasets.splice(0, 1);
+ window.myLine.update();
});
$('#removeData').click(function() {
config.data.labels.splice(-1, 1); // remove the label first
config.data.datasets.forEach(function(dataset, datasetIndex) {
- window.myLine.removeData(datasetIndex, -1);
+ dataset.data.pop();
});
+
+ window.myLine.update();
});