diff --git a/vendor/assets/javascripts/Chart.js b/vendor/assets/javascripts/Chart.js index cd79b6d..4826159 100644 --- a/vendor/assets/javascripts/Chart.js +++ b/vendor/assets/javascripts/Chart.js @@ -3079,7 +3079,7 @@ helpers.each(this.segments,function(segment){ segment.save(); }); - + this.reflow(); this.render(); }, @@ -3474,4 +3474,502 @@ +}).call(this); + + +// Horizontal Bar Chart Type +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + var defaultConfig = { + //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero : true, + + //Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - If there is a stroke on each bar + barShowStroke : true, + + //Number - Pixel width of the bar stroke + barStrokeWidth : 2, + + //Number - Spacing between each of the X value sets + barValueSpacing : 5, + + //Number - Spacing between data sets within X values + barDatasetSpacing : 1, + + //String - A legend template + legendTemplate : "" + + }; + + Chart.HorizontalRectangle = Chart.Element.extend({ + draw : function(){ + var ctx = this.ctx, + halfHeight = this.height/2, + top = this.y - halfHeight, + bottom = this.y + halfHeight, + right = this.left - (this.left - this.x), + halfStroke = this.strokeWidth / 2; + + // Canvas doesn't allow us to stroke inside the width so we can + // adjust the sizes to fit if we're setting a stroke on the line + if (this.showStroke){ + top += halfStroke; + bottom -= halfStroke; + right += halfStroke; + } + + ctx.beginPath(); + + ctx.fillStyle = this.fillColor; + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + // It'd be nice to keep this class totally generic to any rectangle + // and simply specify which border to miss out. + ctx.moveTo(this.left, top); + ctx.lineTo(right, top); + ctx.lineTo(right, bottom); + ctx.lineTo(this.left, bottom); + ctx.fill(); + if (this.showStroke){ + ctx.stroke(); + } + }, + inRange : function(chartX,chartY){ + return (chartX >= this.left && chartX <= this.x && chartY >= (this.y - this.height/2) && chartY <= (this.y + this.height/2)); + } + }); + + Chart.Type.extend({ + name: "HorizontalBar", + defaults : defaultConfig, + initialize: function(data){ + + //Expose options as a scope variable here so we can access it in the ScaleClass + var options = this.options; + + this.ScaleClass = Chart.Scale.extend({ + offsetGridLines : true, + calculateBarX : function(datasetCount, datasetIndex, barIndex){ + //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar + var xWidth = this.calculateBaseWidth(), + xAbsolute = this.calculateX(barIndex) - (xWidth/2), + barWidth = this.calculateBarWidth(datasetCount); + + return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; + }, + calculateBaseWidth : function(){ + return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); + }, + calculateBarWidth : function(datasetCount){ + //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset + var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); + + return (baseWidth / datasetCount); + }, + + calculateBaseHeight : function(){ + return ((this.endPoint - this.startPoint) / this.yLabels.length) - (2*options.barValueSpacing); + }, + calculateBarHeight : function(datasetCount){ + //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset + var baseHeight = this.calculateBaseHeight() - ((datasetCount) * options.barDatasetSpacing); + + return (baseHeight / datasetCount); + }, + + calculateXInvertXY : function(value) { + var scalingFactor = (this.width - Math.round(this.xScalePaddingLeft) - this.xScalePaddingRight) / (this.max - this.min); + return Math.round(this.xScalePaddingLeft) + (scalingFactor * (value - this.min)); + }, + + calculateYInvertXY : function(index){ + return index * ((this.startPoint - this.endPoint) / (this.yLabels.length)); + }, + + calculateBarY : function(datasetCount, datasetIndex, barIndex){ + //Reusable method for calculating the yPosition of a given bar based on datasetIndex & height of the bar + var yHeight = this.calculateBaseHeight(), + yAbsolute = (this.endPoint + this.calculateYInvertXY(barIndex) - (yHeight / 2)) - 5, + barHeight = this.calculateBarHeight(datasetCount); + if (datasetCount > 1) yAbsolute = yAbsolute + (barHeight * (datasetIndex - 1)) - (datasetIndex * options.barDatasetSpacing) + barHeight/2; + return yAbsolute; + }, + + buildCalculatedLabels : function() { + this.calculatedLabels = []; + + var stepDecimalPlaces = helpers.getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.calculatedLabels.push(helpers.template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + }, + + buildYLabels : function(){ + this.buildYLabelCounter = (typeof this.buildYLabelCounter === 'undefined') ? 0 : this.buildYLabelCounter + 1; + this.buildCalculatedLabels(); + if(this.buildYLabelCounter === 0) this.yLabels = this.xLabels; + this.xLabels = this.calculatedLabels; + this.yLabelWidth = (this.display && this.showLabels) ? helpers.longestText(this.ctx,this.font,this.yLabels) : 0; + }, + + calculateX : function(index){ + var isRotated = (this.xLabelRotation > 0), + innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), + valueWidth = innerWidth/(this.steps - ((this.offsetGridLines) ? 0 : 1)), + valueOffset = (valueWidth * index) + this.xScalePaddingLeft; + + if (this.offsetGridLines){ + valueOffset += (valueWidth/2); + } + + return Math.round(valueOffset); + }, + + draw : function(){ + var ctx = this.ctx, + yLabelGap = (this.endPoint - this.startPoint) / this.yLabels.length, + xStart = Math.round(this.xScalePaddingLeft); + if (this.display){ + ctx.fillStyle = this.textColor; + ctx.font = this.font; + helpers.each(this.yLabels,function(labelString,index){ + var yLabelCenter = this.endPoint - (yLabelGap * index), + linePositionY = Math.round(yLabelCenter); + + yLabelCenter -= yLabelGap / 2; + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + if (this.showLabels){ + ctx.fillText(labelString,xStart - 10,yLabelCenter); + } + ctx.beginPath(); + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + linePositionY += helpers.aliasPixel(ctx.lineWidth); + + ctx.moveTo(xStart, linePositionY); + ctx.lineTo(this.width, linePositionY); + ctx.stroke(); + ctx.closePath(); + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + ctx.beginPath(); + ctx.moveTo(xStart - 5, linePositionY); + ctx.lineTo(xStart, linePositionY); + ctx.stroke(); + ctx.closePath(); + + },this); + + helpers.each(this.xLabels,function(label,index){ + var width = this.calculateX(1) - this.calculateX(0); + var xPos = this.calculateX(index) + helpers.aliasPixel(this.lineWidth) - (width / 2), + // Check to see if line/bar here and decide where to place the line + linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + helpers.aliasPixel(this.lineWidth), + isRotated = (this.xLabelRotation > 0); + + ctx.beginPath(); + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.startPoint - 3); + ctx.stroke(); + ctx.closePath(); + + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + + + // Small lines at the bottom of the base grid line + ctx.beginPath(); + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.endPoint + 5); + ctx.stroke(); + ctx.closePath(); + + ctx.save(); + ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); + ctx.rotate(helpers.radians(this.xLabelRotation)*-1); + ctx.font = this.font; + ctx.textAlign = (isRotated) ? "right" : "center"; + ctx.textBaseline = (isRotated) ? "middle" : "top"; + ctx.fillText(label, 0, 0); + ctx.restore(); + },this); + + } + } + + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; + + this.eachBars(function(bar){ + bar.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activeBars, function(activeBar){ + activeBar.fillColor = activeBar.highlightFill; + activeBar.strokeColor = activeBar.highlightStroke; + }); + this.showTooltip(activeBars); + }); + } + + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.BarClass = Chart.HorizontalRectangle.extend({ + strokeWidth : this.options.barStrokeWidth, + showStroke : this.options.barShowStroke, + ctx : this.chart.ctx + }); + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset,datasetIndex){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + bars : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.bars.push(new this.BarClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.strokeColor, + fillColor : dataset.fillColor, + highlightFill : dataset.highlightFill || dataset.fillColor, + highlightStroke : dataset.highlightStroke || dataset.strokeColor + })); + },this); + + },this); + + this.buildScale(data.labels); + + this.BarClass.prototype.left = Math.round(this.scale.xScalePaddingLeft); + + this.eachBars(function(bar, index, datasetIndex){ + helpers.extend(bar, { + x: Math.round(this.scale.xScalePaddingLeft), + y : this.scale.calculateBarY(this.datasets.length, datasetIndex, index), + height : this.scale.calculateBarHeight(this.datasets.length) + }); + bar.save(); + }, this); + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + + this.eachBars(function(bar){ + bar.save(); + }); + this.render(); + }, + eachBars : function(callback){ + helpers.each(this.datasets,function(dataset, datasetIndex){ + helpers.each(dataset.bars, callback, this, datasetIndex); + },this); + }, + getBarsAtEvent : function(e){ + var barsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset){ + barsArray.push(dataset.bars[barIndex]); + }, + barIndex; + + for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { + for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { + if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ + helpers.each(this.datasets, datasetIterator); + return barsArray; + } + } + } + + return barsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachBars(function(bar){ + values.push(bar.value); + }); + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange: function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + this.scale = new this.ScaleClass(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].bars.push(new this.BarClass({ + value : value, + label : label, + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), + y: this.scale.endPoint, + width : this.scale.calculateBarWidth(this.datasets.length), + base : this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].strokeColor, + fillColor : this.datasets[datasetIndex].fillColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.bars.shift(); + },this); + this.update(); + }, + reflow : function(){ + helpers.extend(this.BarClass.prototype,{ + y: this.scale.endPoint, + base : this.scale.endPoint + }); + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + this.scale.draw(easingDecimal); + + //Draw all the bars for each dataset + helpers.each(this.datasets,function(dataset,datasetIndex){ + helpers.each(dataset.bars,function(bar,index){ + if (bar.hasValue()){ + bar.left = Math.round(this.scale.xScalePaddingLeft); + //Transition then draw + bar.transition({ + x : this.scale.calculateXInvertXY(bar.value), + y : this.scale.calculateBarY(this.datasets.length, datasetIndex, index), + height : this.scale.calculateBarHeight(this.datasets.length) + }, easingDecimal).draw(); + } + },this); + + },this); + } + }); + + }).call(this);