/**
* A chart (line / column etc.)
* <pre><strong>options:</strong> {
* element : «Element | ID»,
* name : «String»,
* (TODO many more)
* }
* </pre>
* @constructor
* @param {Object} options The options
*/
hui.ui.Chart = function(options) {
this.options = options = options || {};
this.element = hui.get(options.element);
this.body = {
width : undefined,
height : undefined,
paddingTop : 10,
paddingBottom : 30,
paddingLeft : 10,
paddingRight : 10,
innerPaddingVertical : 10,
innerPaddingHorizontal : 10
};
this.style = {
border : true,
background : true,
colors : ['#36a','#69d','#acf'],
legends : { position: 'right' , left: 0, top: 0 },
pie : { radiusFactor: 0.9 , valueInLegend: false , left: 0, top: 0 }
};
this.xAxis = { labels:[], grid:true, concentration: 0.8 , maxLabels: 12};
this.yAxis = { min:0, max:0, steps:8, above:false , factor: 10};
this.dataSets = [];
this.data = null;
hui.ui.extend(this);
if (this.options.source) {
this.options.source.listen(this);
}
};
hui.ui.Chart.create = function(options) {
options.element = hui.build('div',{
'class' : 'hui_chart',
parent : hui.get(options.parent),
style : 'width: 100%; height: 100%;'
});
return new hui.ui.Chart(options);
};
hui.ui.Chart.prototype = {
addDataSet : function(dataSet) {
this.dataSets[this.dataSets.length] = dataSet;
},
setXaxisLabels : function(labels) {
for (var i=0; i < labels.length; i++) {
this.xAxis.labels[this.xAxis.labels.length] = {key:labels[i],label:labels[i]};
}
},
setData : function(data) {
if (!data.dataSets) {
this.data = hui.ui.Chart.Util.convertData(data);
} else {
this.data = data;
}
},
render : function() {
var renderer = new hui.ui.Chart.Renderer(this);
renderer.render();
},
$$layout : function() {
this.render();
},
$objectsLoaded : function(data) {
this.setData(data);
this.render();
}
};
//////////////////////// Data ////////////////////
hui.ui.Chart.Data = function(options) {
this.xAxis = hui.override({ labels:[], grid:true, concentration:0.8 , maxLabels:12},options.xAxis);
this.yAxis = hui.override({ min:0, max:0, steps:8, above:false , factor: 10},options.yAxis);
this.dataSets = [];
};
hui.ui.Chart.Data.prototype = {
addDataSet : function(set) {
this.dataSets.push(set);
}
};
///////////////////// Data set ////////////////////
hui.ui.Chart.DataSet = function(options) {
options = options || {};
this.dataSets = [];
this.entries = options.entries || [];
this.legend = null;
this.style = {type:options.type || 'line'};
};
hui.ui.Chart.DataSet.prototype = {
addDataSet : function(dataSet) {
this.dataSets[this.dataSets.length] = dataSet;
},
setLegend : function(legend) {
this.legend = legend;
},
isMultiDimensional : function() {
return this.dataSets.length>0;
},
addEntry : function(key,value) {
this.entries[this.entries.length] = {key:key,value:value};
},
setValues : function(graph,values) {
for (var i=0; i < graph.xAxis.labels.length; i++) {
if (values[i]) {
this.addEntry(graph.xAxis.labels[i].key,values[i]);
}
}
},
getEntryValue : function(key) {
var value = 0;
for (var i=0;i<this.entries.length;i++) {
if (this.entries[i].key==key) {
return this.entries[i].value;
}
}
return value;
},
getEntryValue2D : function(key) {
var value = [];
for (var i=0;i<this.dataSets.length;i++) {
var set = this.dataSets[i];
for (var j=0;j<set.entries.length;j++) {
if (set.entries[j].key==key) {
value[i] = set.entries[j].value;
}
}
if (!value[i]) {
value[i]=0;
}
}
return value;
},
keysToValues : function(keys) {
var values = [];
for (var i = 0; i < keys.length; i++) {
values[i] = this.getEntryValue(keys[i].key);
}
return values;
},
keysToValues2D : function(keys) {
var values = [];
for (var i = 0; i < keys.length; i++) {
values[i] = this.getEntryValue2D(keys[i].key);
}
return values;
},
getValueRange : function(keys) {
var vals = [];
if (this.isMultiDimensional()) {
var vals2D = this.keysToValues2D(keys);
for (var i=0; i < vals2D.length; i++) {
var sum = 0;
for (var j=0; j < vals2D[i].length; j++) {
sum+=vals2D[i][j];
}
vals[i] = sum;
}
} else {
vals = this.keysToValues(keys);
}
var min = Number.MAX_VALUE,
max = Number.MIN_VALUE;
for (var k=0; k < vals.length; k++) {
min = Math.min(min, vals[k]);
max = Math.max(max, vals[k]);
}
return {min:min,max:max};
},
getSubLegends : function() {
var value = [];
for (var i = 0; i < this.dataSets.length; i++) {
value[i] = this.dataSets[i].legend;
}
return value;
}
};
/*********************************************************************/
/* Renderer */
/*********************************************************************/
hui.ui.Chart.Renderer = function(chart) {
this.chart = chart;
this.crisp = false;
this.legends = [];
this.state = { numColumns:0, currColumn:0, xLabels:[], yLabels:[], body:{left:0}, innerBody:{}, coordinateSystem: false, currColor:0 };
this.width = null;
this.height = null;
};
hui.ui.Chart.Renderer.prototype = {
_registerLegend : function(color,label) {
this.legends[this.legends.length] = {color:color,label:label};
},
_buildInnerBody : function() {
var body = this.chart.body;
var xLabels = this.state.xLabels;
var space = 0;
if (this.state.numColumns>0) {
space = ( this.width - 2 * body.innerPaddingHorizontal - body.paddingLeft - body.paddingRight ) / xLabels.length;
}
var innerBody = {
left : (body.innerPaddingHorizontal + this.state.body.left + space/2),
top : (body.paddingTop + body.innerPaddingVertical),
width : (this.state.body.width-2 * body.innerPaddingHorizontal - space),
height : (this.state.body.height - body.innerPaddingVertical * 2 )
};
return innerBody;
},
_buildBody : function() {
var body = this.chart.body,
left = body.paddingLeft + this.state.yLabelWidth;
return {
left : left,
top : body.paddingTop,
width : this.width - left - body.paddingRight,
height : this.height - body.paddingTop - body.paddingBottom,
right : this.width - body.paddingRight,
bottom : this.height - body.paddingBottom
};
}
};
hui.ui.Chart.Renderer.prototype.render = function() {
this.width = this.chart.body.width || this.chart.element.clientWidth;
this.height = this.chart.body.height || this.chart.element.clientHeight;
hui.dom.clear(this.chart.element);
this.canvas = hui.build('canvas',{parent:this.chart.element,width:this.width,height:this.height});
if (!this.canvas.getContext) {
return;
}
this.ctx = this.canvas.getContext("2d");
if (!hui.isDefined(this.chart.data)) {
return;
}
var i, dataSet;
// Extract basic info about the chart
for (i=0;i<this.chart.data.dataSets.length;i++) {
dataSet = this.chart.data.dataSets[i];
if (dataSet.style.type=='line' || dataSet.style.type=='column') {
this.state.coordinateSystem = true;
}
if (dataSet.style.type=='column') {
this.state.numColumns++;
}
}
this.state.xLabels = this.chart.data.xAxis.labels;
this.state.yLabels = hui.ui.Chart.Util.generateYLabels(this.chart);
this.state.yLabelWidth = 0;
for (i = 0; i < this.state.yLabels.length; i++) {
this.state.yLabelWidth = Math.max(this.state.yLabelWidth, String(this.state.yLabels[i]).length * 5);
}
this.state.yLabelWidth+=5;
this.state.body = this._buildBody();
this.state.innerBody = this._buildInnerBody();
// Render the coordinate system (below)
if (this.state.coordinateSystem) {
this.renderBody();
}
// Loop through data sets and render them
var xLabels = this.state.xLabels;
for (i = 0; i < this.chart.data.dataSets.length; i++) {
dataSet = this.chart.data.dataSets[i];
var values, legend;
if (dataSet.style.type=='line') {
values = dataSet.keysToValues(xLabels);
this.renderLineGraph( { values:values, style:dataSet.style , legend:dataSet.legend } );
} else if (dataSet.style.type=='column') {
if (dataSet.isMultiDimensional()) {
values = dataSet.keysToValues2D(xLabels);
legend = dataSet.getSubLegends();
} else {
values = dataSet.keysToValues(xLabels);
legend = dataSet.legend;
}
this.renderColumnGraph( { values:values, style:dataSet.style , legend: legend} );
} else if (dataSet.style.type=='pie') {
values = dataSet.keysToValues(xLabels);
this.renderPie( { values:values, style:dataSet.style } );
}
}
// Render the coordinate system (above)
if (this.shouldRenderCoordinateSystem) {
this.renderPostBody();
}
// Render possible lengends
this.renderLegends();
};
/**
* Renders a legend box
*/
hui.ui.Chart.Renderer.prototype.renderLegends = function() {
if (this.legends.length>0) {
var position = this.chart.style.legends.position;
var box = hui.build('div',{style:{position:'absolute',zIndex:5,width:this.width+'px'}});
var html='<div class="hui_chart_legends" style="margin-right: '+(5-this.chart.style.legends.left)+'px; margin-top: '+(5+this.chart.style.legends.top)+'px;">';
for (var i=0;i<this.legends.length;i++) {
var style = '';
if (position=='bottom') {
style = 'padding: 2px; padding-right: 8px; float: left; white-space: nowrap;';
if (i==this.legends.length-1) {
style+='padding-right: 3px';
}
} else {
style = 'padding: 2px;';
}
html+='<div class="hui_chart_legend" style="'+style+'"><em style="background: '+this.legends[i].color+';"></em><span>'+this.legends[i].label+'</span></div>';
}
html+='</div>';
box.innerHTML = html;
if (position=='right') {
this.canvas.parentNode.insertBefore(box,this.canvas);
} else if (position=='bottom') {
this.canvas.parentNode.appendChild(box);
var y = document.createElement('div');
y.appendChild(box);
this.canvas.parentNode.appendChild(y);
}
}
};
/**
* Renders the body of the chart
*/
hui.ui.Chart.Renderer.prototype.renderBody = function() {
var body = this.chart.body,
stroke = 'rgb(255,255,255)',
background = 'rgb(240,240,240)',
state = this.state,
innerBody = this.state.innerBody;
stroke = '#eee'; // TODO Make this configurable
background = '#fff';
if (this.chart.style.background) {
this.ctx.fillStyle = background;
this.ctx.fillRect(
state.body.left,
state.body.top,
state.body.width,
state.body.height
);
}
var mod = 1;
/* Build X-axis*/
var xLabels = this.state.xLabels;
if (xLabels.length>this.chart.data.xAxis.maxLabels) {
mod = Math.ceil(xLabels.length/this.chart.data.xAxis.maxLabels);
}
this.ctx.strokeStyle=stroke;
for (var i = 0; i < xLabels.length; i++) {
var left = i * ((innerBody.width) / (xLabels.length - 1)) + innerBody.left;
left = Math.round(left);
if (mod < 10 || (i % mod) === 0) {
// Draw grid
if (this.chart.data.xAxis.grid) {
this.ctx.beginPath();
this.ctx.moveTo(0.5 + left, state.body.top + 0.5);
this.ctx.lineTo(0.5 + left, state.body.top + 0.5 + state.body.height);
this.ctx.stroke();
this.ctx.closePath();
}
}
if ((i % mod) === 0) {
// Draw label
hui.build('span',{
'class' : 'hui_chart_label',
text : xLabels[i].label,
before : this.canvas,
style : {
marginLeft : (left - 25) + 'px',
marginTop : (state.body.bottom + 4) + 'px',
color : '#999'
}
});
}
}
this.ctx.strokeStyle=stroke;
/* Build Y-axis*/
var yLabels = this.state.yLabels.concat();
yLabels.reverse();
var top;
for (i = 0; i < yLabels.length; i++) {
// Draw grid
top = i * ((state.body.height - body.innerPaddingVertical * 2) / (yLabels.length - 1)) + body.paddingTop + body.innerPaddingVertical;
top = Math.round(top);
if (!this.chart.data.yAxis.above) {
this.ctx.beginPath();
this.ctx.moveTo(0.5 + state.body.left, top + 0.5);
this.ctx.lineTo(0.5 + state.body.right, top + 0.5);
this.ctx.stroke();
this.ctx.closePath();
}
// Draw label
var label = hui.build('span',{text:yLabels[i],style:{
position: 'absolute',
textAlign : 'right',
width : (this.state.yLabelWidth - 5) + 'px',
font : '9px Tahoma',
marginTop : (top - 5) +'px',
marginLeft : body.paddingLeft+'px',
color : '#999'
}});
this.canvas.parentNode.insertBefore(label,this.canvas);
}
// Draw a line at 0 if
if (!this.chart.data.yAxis.above && yLabels[0] > 0 && yLabels[yLabels.length-1] < 0) {
top = (state.body.height - body.innerPaddingVertical * 2) * yLabels[0] / (yLabels[0] - yLabels[yLabels.length - 1]) + body.paddingTop + body.innerPaddingVertical;
top = Math.round(top);
this.ctx.lineWidth = 2;
this.ctx.strokeStyle=stroke;
this.ctx.beginPath();
this.ctx.moveTo(0.5 + state.body.left, top);
this.ctx.lineTo(0.5 + state.body.right, top);
this.ctx.stroke();
this.ctx.closePath();
}
};
hui.ui.Chart.Renderer.prototype.renderPostBody = function() {
var body = this.chart.body;
if (this.chart.data.yAxis.above) {
this.ctx.strokeStyle='rgb(240,240,240)';
var yLabels = this.state.yLabels.concat();
yLabels.reverse();
var top;
for (var i = 0; i < yLabels.length; i++) {
top = i * ((this.height - body.innerPaddingVertical * 2 - body.paddingTop - body.paddingBottom) / (yLabels.length - 1)) + body.paddingTop + body.innerPaddingVertical;
top = Math.round(top);
this.ctx.lineWidth = 1;
this.ctx.beginPath();
this.ctx.moveTo(0.5 + body.paddingLeft, top + 0.5);
this.ctx.lineTo(0.5 + this.width - body.paddingRight, top + 0.5);
this.ctx.stroke();
this.ctx.closePath();
}
if (yLabels[0] > 0 && yLabels[yLabels.length - 1] < 0) {
top = (this.height - body.innerPaddingVertical * 2 - body.paddingTop - body.paddingBottom) * yLabels[0] / (yLabels[0] - yLabels[yLabels.length - 1]) + body.paddingTop + body.innerPaddingVertical;
top = Math.round(top);
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = 'rgb(255,255,255)';
this.ctx.beginPath();
this.ctx.moveTo(0.5 + body.paddingLeft,top);
this.ctx.lineTo(0.5 + this.width - body.paddingRight,top);
this.ctx.stroke();
this.ctx.closePath();
}
}
if (this.chart.style.border) {
this.ctx.lineWidth = 1;
this.ctx.strokeStyle='rgb(230,230,230)';
this.ctx.strokeRect(body.paddingLeft + 0.5, body.paddingTop + 0.5, this.width-body.paddingLeft - body.paddingRight, this.height - body.paddingTop - body.paddingBottom);
}
};
hui.ui.Chart.Renderer.prototype.renderLineGraph = function(data) {
var values = data.values;
var xLabels = this.state.xLabels;
var yLabels = this.state.yLabels;
var yMin = yLabels[0];
var yMax = yLabels[yLabels.length - 1];
var body = this.chart.body;
var innerBody = this.state.innerBody;
var color;
if (data.style.colors) {
color = data.style.colors[0];
} else {
color = this.chart.style.colors[this.state.currColor];
if (this.state.currColor + 2 > this.chart.style.colors.length) {
this.state.currColor = 0;
} else {
this.state.currColor++;
}
}
this.ctx.strokeStyle = color;
this.ctx.lineWidth = data.width ? data.width : 3;
this.ctx.lineCap = this.ctx.lineJoin = 'round';
this.ctx.beginPath();
for (var i = 0; i < xLabels.length; i++) {
var amount = (values[i] === undefined ? 0 : values[i]);
var value = (amount - yMin) / (yMax - yMin);
var top = this.height - value * (innerBody.height) - body.innerPaddingVertical - body.paddingBottom;
var left = i * (innerBody.width / (xLabels.length - 1)) + innerBody.left;
if (i === 0) {
this.ctx.moveTo(left + 0.5, top + 0.5);
} else {
this.ctx.lineTo(left + 0.5, top + 0.5);
}
}
this.ctx.stroke();
this.ctx.closePath();
if (data.legend) {
this._registerLegend(color,data.legend);
}
};
hui.ui.Chart.Renderer.prototype.renderColumnGraph = function(data) {
var values = data.values;
var xLabels = this.state.xLabels;
var yLabels = this.state.yLabels;
var yMin = yLabels[0];
var yMax = yLabels[yLabels.length-1];
var body = this.chart.body;
var colors = data.style.colors ? data.style.colors : this.chart.style.colors;
this.state.currColumn++;
var innerBody = this.state.innerBody;
var space = (this.width - body.paddingLeft - body.paddingRight) / xLabels.length * this.chart.data.xAxis.concentration;
var thickness = space / this.state.numColumns;
this.ctx.lineCap = this.ctx.lineJoin = 'round';
this.ctx.beginPath();
for (var i = 0; i < xLabels.length; i++) {
if (values[i]) {
var colorIndex = 0;
var currTop = 0;
if (values[i] instanceof Array) {
for (var j = 0; j < values[i].length; j++) {
var val = values[i][j];
currTop += this.renderOneColumn(val, colors[colorIndex], body, innerBody, yMin, yMax, currTop, i, xLabels, space, thickness);
if (colorIndex+2>colors.length) {
colorIndex = 0;
} else {
colorIndex++;
}
}
} else {
currTop += this.renderOneColumn(values[i], colors[colorIndex], body, innerBody, yMin, yMax, currTop, i, xLabels, space, thickness);
}
}
}
this.ctx.stroke();
this.ctx.closePath();
if (data.legend && data.legend instanceof Array) {
for (var k = 0; k < data.legend.length; k++) {
this._registerLegend(colors[k], data.legend[k]);
}
} else if (data.legend) {
this._registerLegend(colors[0], data.legend);
}
};
hui.ui.Chart.Renderer.prototype.renderOneColumn = function(val,color,body,innerBody,yMin,yMax,currTop,i,xLabels,space,thickness) {
var value = (val - yMin) / (yMax - yMin);
var height, top;
if (yMin<=0 && val<=0) {
top = innerBody.top + (innerBody.height) * yMax / (yMax - yMin) + currTop;
height = innerBody.height * Math.abs(val) / (yMax - yMin);
} else if (yMin <= 0) {
top = this.height - body.innerPaddingVertical - body.paddingBottom - value * (innerBody.height) - currTop;
height = (innerBody.height) * Math.abs(val) / (yMax - yMin);
}
else {
top = this.height - value * (this.height - body.innerPaddingVertical * 2 - body.paddingTop - body.paddingBottom) - body.innerPaddingVertical - body.paddingBottom - currTop;
height = (this.height - body.paddingBottom - top);
}
var left = i* ((innerBody.width) / (xLabels.length - 1)) + innerBody.left;
this.ctx.fillStyle = color;
if (this.crisp) {
this.ctx.fillRect(Math.round(left - space / 2 + thickness * (this.state.currColumn - 1)), Math.floor(top), Math.ceil(thickness), Math.ceil(height));
} else {
this.ctx.fillRect(left - space / 2 + thickness * (this.state.currColumn - 1), top, thickness, height);
}
return height;
};
hui.ui.Chart.Renderer.prototype.renderPie = function(data) {
var values = data.values;
var colors = data.style.colors ? data.style.colors : this.chart.style.colors;
var total = hui.ui.Chart.Util.arraySum(values);
var colorIndex = 0;
var current = Math.PI * 1.5;
var cTop = this.height / 2 + this.chart.style.pie.top;
var cLeft = this.width / 2 + this.chart.style.pie.left;
var radius = this.height / 2 * this.chart.style.pie.radiusFactor;
for (var i = 0; i < values.length; i++) {
this.ctx.beginPath();
var color = colors[colorIndex];
this.ctx.fillStyle = color;
var rads = values[i] / total * (Math.PI * 2);
this.ctx.moveTo(cLeft, cTop);
this.ctx.arc(cLeft, cTop, radius, current, current + rads, false);
this.ctx.lineTo(cLeft, cTop);
this.ctx.fill();
this.ctx.closePath();
current+=rads;
if (!true) {
this._registerLegend(color, this.state.xLabels[i].label);
} else {
this._registerLegend(color, values[i] + ' ' + this.state.xLabels[i].label);
}
if (colorIndex + 2 > colors.length) {
colorIndex = 0;
} else {
colorIndex++;
}
}
};
/*********************************************************************/
/* Utitlities */
/*********************************************************************/
hui.ui.Chart.Util = function() {};
hui.ui.Chart.Util.generateYLabels = function(graph) {
var range = hui.ui.Chart.Util.getYrange(graph);
var labels = [];
for (var i = 0; i <= graph.yAxis.steps; i++) {
labels[labels.length] = Math.round(range.min + (range.max - range.min) / graph.yAxis.steps * i);
}
return labels;
};
hui.ui.Chart.Util.getYrange = function(graph) {
var min = graph.yAxis.min,
max = graph.yAxis.max,
data = graph.data;
for (var i = 0; i < data.dataSets.length; i++) {
var range = data.dataSets[i].getValueRange(data.xAxis.labels);
min = Math.min(min, range.min);
max = Math.max(max, range.max);
}
var factor = max / graph.yAxis.steps;
if (factor < graph.yAxis.factor) {
factor = Math.ceil(factor);
} else {
factor = graph.yAxis.factor;
}
if (max != Number.MIN_VALUE) {
max = Math.ceil(max / factor / graph.yAxis.steps) * factor * graph.yAxis.steps;
} else {
max = graph.yAxis.steps;
}
return {min: min, max : max};
};
hui.ui.Chart.Util.arraySum = function(values) {
var total = 0;
for (var i = 0; i < values.length; i++) {
total += values[i];
}
return total;
};
/** Converts a simple data-representation into a class-based stucture */
hui.ui.Chart.Util.convertData = function(obj) {
var labels = [],keys = [];
var i;
for (i = 0; i < obj.sets.length; i++) {
var set = obj.sets[i];
if (hui.isArray(set.entries)) {
for (var j=0; j < set.entries.length; j++) {
var entry = set.entries[j];
if (!hui.array.contains(keys, entry.key)) {
keys.push(entry.key);
labels.push({key: entry.key, label: entry.label || entry.key});
}
}
} else {
for (var key in set.entries) {
if (!hui.array.contains(keys, key)) {
keys.push(key);
labels.push({key: key, label: key});
}
}
}
}
var options = {xAxis: {labels: labels}};
if (obj.axis && obj.axis.x && obj.axis.x.time===true) {
options.xAxis.resolution = 'time';
}
if (obj.axis && obj.axis.x && hui.isArray(obj.axis.x.labels)) {
options.xAxis.labels = obj.axis.x.labels;
}
var data = new hui.ui.Chart.Data(options);
for (i = 0; i < obj.sets.length; i++) {
var setData = obj.sets[i];
var dataSet = new hui.ui.Chart.DataSet({type : setData.type});
if (hui.isArray(setData.entries)) {
for (var k = 0; k < setData.entries.length; k++) {
var dataEntry = setData.entries[k];
dataSet.addEntry(dataEntry.key, dataEntry.value);
}
} else {
for (var setKey in setData.entries) {
dataSet.addEntry(setKey, setData.entries[setKey]);
}
}
data.addDataSet(dataSet);
}
return data;
};