(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 : "
-legend\"><% for (var i=0; i- \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
<%}%>
"
};
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),
drawHorizontalLine = this.showHorizontalLines;
yLabelCenter -= yLabelGap / 2;
ctx.textAlign = "right";
ctx.textBaseline = "middle";
if (this.showLabels){
ctx.fillText(labelString,xStart - 10,yLabelCenter);
}
if (index === 0 && !drawHorizontalLine) {
drawHorizontalLine = true;
}
if (drawHorizontalLine){
ctx.beginPath();
}
if (index > 0){
// This is a grid line in the centre, so drop that
ctx.lineWidth = this.gridLineWidth;
ctx.strokeStyle = this.gridLineColor;
} else {
// This is the first line on the scale
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
}
linePositionY += helpers.aliasPixel(ctx.lineWidth);
if(drawHorizontalLine){
ctx.moveTo(xStart, linePositionY);
ctx.lineTo(this.width, linePositionY);
ctx.stroke();
ctx.closePath();
}
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.lineColor;
ctx.beginPath();
ctx.moveTo(xStart - 5, linePositionY);
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);