(function() { "use strict"; var root = this, Chart = root.Chart, helpers = Chart.helpers; Chart.defaults.scale = { display: true, // 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, }, // scale label scaleLabel: { fontColor: '#666', fontFamily: 'Helvetica Neue', fontSize: 12, fontStyle: 'normal', // actual label labelString: '', // display property show: false, }, // label settings ticks: { beginAtZero: false, fontSize: 12, fontStyle: "normal", fontColor: "#666", fontFamily: "Helvetica Neue", maxRotation: 90, minRotation: 20, mirror: false, padding: 10, reverse: false, show: true, callback: function(value) { return '' + value; }, }, }; 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(); this.beforeTickToLabelConversion(); this.convertTicksToLabels(); this.afterTickToLabelConversion(); // 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()) { // Reset position before calculating rotation this.width = this.maxWidth; this.left = 0; this.right = this.width; } else { this.height = this.maxHeight; // Reset position before calculating rotation this.top = 0; this.bottom = this.height; } // Reset padding this.paddingLeft = 0; this.paddingTop = 0; this.paddingRight = 0; this.paddingBottom = 0; }, afterSetDimensions: helpers.noop, // beforeBuildTicks: helpers.noop, buildTicks: helpers.noop, afterBuildTicks: helpers.noop, beforeTickToLabelConversion: helpers.noop, convertTicksToLabels: function() { // Convert ticks to strings this.ticks = this.ticks.map(function(numericalTick, index, ticks) { if (this.options.ticks.userCallback) { return this.options.ticks.userCallback(numericalTick, index, ticks); } return this.options.ticks.callback(numericalTick, index, ticks); }, this); }, afterTickToLabelConversion: 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; } 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 } // Are we showing a title for the scale? if (this.options.scaleLabel.show) { if (this.isHorizontal()) { this.minSize.height += (this.options.scaleLabel.fontSize * 1.5); } else { this.minSize.width += (this.options.scaleLabel.fontSize * 1.5); } } 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 longestLabelWidth = helpers.longestText(this.ctx, labelFont, this.ticks); var labelHeight = (Math.sin(helpers.toRadians(this.labelRotation)) * longestLabelWidth) + 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. When rotated, ticks are right aligned which means that the right padding is dominated // by the font height var cosRotation = Math.cos(helpers.toRadians(this.labelRotation)); var sinRotation = Math.sin(helpers.toRadians(this.labelRotation)); this.paddingLeft = this.labelRotation !== 0 ? (cosRotation * firstLabelWidth) + 3 : firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges this.paddingRight = this.labelRotation !== 0 ? (sinRotation * (this.options.ticks.fontSize / 2)) + 3 : lastLabelWidth / 2 + 3; // when rotated } 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"; }, // Get the correct value. NaN bad inputs, If the value type is object get the x or y based on whether we are horizontal or not getRightValue: function getRightValue(rawValue) { // Null and undefined values first if (rawValue === null || typeof(rawValue) === 'undefined') { return NaN; } // isNaN(object) returns true, so make sure NaN is checking for a number if (typeof(rawValue) === 'number' && isNaN(rawValue)) { return NaN; } // If it is in fact an object, dive in one more level if (typeof(rawValue) === "object") { return getRightValue(this.isHorizontal() ? rawValue.x : rawValue.y); } // Value is good, return it return rawValue; }, // Used to get the value to display in the tooltip for the data at the given index // function getLabelForIndex(index, datasetIndex) getLabelForIndex: helpers.noop, // 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); } }, // 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 = this.labelRotation !== 0; var skipRatio; var scaleLabelX; var scaleLabelY; // Make sure we draw text in the correct color and font this.ctx.fillStyle = this.options.ticks.fontColor; var labelFont = helpers.fontString(this.options.ticks.fontSize, this.options.ticks.fontStyle, this.options.ticks.fontFamily); 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; 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.options.position === "top" ? this.bottom - 10 : this.top + 10); this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1); this.ctx.font = labelFont; this.ctx.textAlign = (isRotated) ? "right" : "center"; this.ctx.textBaseline = (isRotated) ? "middle" : this.options.position === "top" ? "bottom" : "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.fillStyle = this.options.scaleLabel.fontColor; // render in correct colour 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 == "right" ? this.left : this.right - 5; var xTickEnd = this.options.position == "right" ? this.left + 5 : this.right; helpers.each(this.ticks, function(label, index) { // If the callback returned a null or undefined value, do not draw this line if (label === undefined || label === null) { return; } var yLineValue = this.getPixelForTick(index); // xvalues for grid lines 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) { var xLabelValue; var yLabelValue = this.getPixelForTick(index, this.options.gridLines.offsetGridLines); // x values for ticks (need to consider offsetLabel option) this.ctx.save(); if (this.options.position == "left") { if (this.options.ticks.mirror) { xLabelValue = this.right + this.options.ticks.padding; this.ctx.textAlign = "left"; } else { xLabelValue = this.right - this.options.ticks.padding; this.ctx.textAlign = "right"; } } else { // right side if (this.options.ticks.mirror) { xLabelValue = this.left - this.options.ticks.padding; this.ctx.textAlign = "right"; } else { xLabelValue = this.left + this.options.ticks.padding; this.ctx.textAlign = "left"; } } this.ctx.translate(xLabelValue, yLabelValue); this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1); this.ctx.font = labelFont; 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.fillStyle = this.options.scaleLabel.fontColor; // render in correct colour 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);