// for debugging purpose only var c = function(){ console.log.apply(console, arguments); }; // the main class! var Graph = function(container, width, height) { var context = null; var contextType = 'none'; var data; var __this__ = this; var __defaultoptions__ = {color:'#000', size:1, background:'rgba(0,0,0,0.5)', align:'center'}; var __options__ = {}; var __methods__ = {canvas:{}, raphael:{}}; var cache = []; __methods__.canvas.size = function(width, height) { context.canvas.width = width; context.canvas.height = height; __options__.width = width; __options__.height = height; }; __methods__.canvas.get = function() { return __options__; } __methods__.canvas.set = function(options) { options.color && (context.strokeStyle = options.color); options.color && (context.fillStyle = options.color); // options.size && }; __methods__.canvas.reset = function() { }; __methods__.canvas.line = function(x1, y1, x2, y2, options) { options || (options = {}); context.lineCap = 'round'; context.lineWidth = getSize(options.size || 1); context.strokeStyle = options.color || __options__.color; context.beginPath(); context.moveTo(getX(x1), getY(y1)); context.lineTo(getX(x2), getY(y2)); context.stroke(); }; __methods__.canvas.rect = function(x1, y1, x2, y2, options) { options || (options = {}); context.lineCap = 'round'; context.lineWidth = getSize(options.size || __options__.size); context.fillStyle = options.color || __options__.color; context.fillRect( getX(x1), getY(y1), getX(x2)-getX(x1), getY(y2)-getY(y1) ); return __this__; }; __methods__.canvas.text = function(x, y, text, options) { options = options || {}; var size = 16 * (options.size || __options__.size); if (scales && scales.size) { size *= scales.size; } size = Math.round(10 * size) / 10; context.font = size + 'px sans-serif'; context.fillStyle = options.color || __options__.color; context.textAlign = options.align || __options__.align; context.textAlign = options.align || __options__.align; context.textBaseline = 'middle'; context.fillText(text, getX(x), getY(y)); return __this__; }; __methods__.canvas.fill = function(background) { context.fillStyle = background || __options__.background; context.fillStyle = 'white'; context.fillRect(0, 0, __options__.width, __options__.height); }; __methods__.canvas.clear = function() { var emptyImage = context.createImageData(context.canvas.width, context.canvas.height); context.putImageData(emptyImage, 0, 0); }; this.size = function(width, height) { __options__.width = width; __options__.height = height; __options__.size = Math.sqrt(__options__.width * __options__.height) / 800; // container.style.width = width + 'px'; // container.style.height = height + 'px'; __this__._size(width, height); __this__.draw(); return __this__; }; this.draw = function() { // clear the canvas __this__._clear(); // redraw what is in the cache for (var i=0; i<cache.length; i++) { var item = cache[i]; __this__['_' + item.method].apply(__this__, item.arguments); } return __this__; }; this.clear = function(keepCaches) { if (!keepCaches) { cache = []; } __this__._clear(); }; var scales = []; var getX = function(x) { var scale = scales[0]; switch (scale.type) { case 'numeric': return (x - scale.min) * __options__.width / scale.span; case 'discrete': return scale.values[x]; default: return x; } }; var getY = function(y) { var scale = scales[1]; switch (scale.type) { case 'numeric': return (scale.max - y) * __options__.height / scale.span; case 'discrete': return scale.values[y]; default: return y; } }; var getSize = function(size) { return size * __options__.size; }; /*this.scale = function(left, top, right, bottom) { if (left === undefined) { // for clearing purpose scale = null; } else { // store new scaling scale = { left: left, right: right, width: (right-left), top: top, bottom: bottom, height: (bottom-top), size: Math.sqrt(width * height) }; } // repaint everything __this__.draw(); return __this__; };*/ this.set = function(options) { cache.push({ method: 'set', arguments: [options] }); for (var key in options) { __options__[key] = options[key]; } __this__._set(options); return __this__; }; this.line = function(x1, y1, x2, y2, options) { cache.push({ method: 'line', arguments: [x1, y1, x2, y2, options] }); __this__._line( x1, y1, x2, y2, options ); return __this__; }; this.text = function(x, y, text, options) { cache.push({ method: 'text', arguments: [x, y, text, options] }); __this__._text(x, y, text, options); return __this__; }; this.rect = function(x1, y1, x2, y2, options) { cache.push({ method: 'rect', arguments: [x1, y1, x2, y2, options] }); __this__._rect(x1, y1, x2, y2, options); return __this__; }; this.fill = function(background) { __this__.clear(); cache.push({ method: 'fill', arguments: [background] }); __this__._fill(background); return __this__; }; this.clear = function() { cache = []; plottingData = { extrema: { xMin: +Number.MAX_VALUE, xMax: -Number.MAX_VALUE, yMin: +Number.MAX_VALUE, yMax: -Number.MAX_VALUE, }, datasets: [] }; __this__._clear(); return __this__; }; var __datasets__ = {}; this.feed = function(datasets) { // get the dimensions & types __datasets__.dimensions = datasets[0].data[0].length; __datasets__.types = []; for (var k=0; k<__datasets__.dimensions; k++) { __datasets__.types.push(typeof(datasets[0].data[0][k])); } // extract values __datasets__.values = []; for (var k=0; k<__datasets__.dimensions; k++) { values = []; for (var i=0; i<datasets.length; i++) { var data = datasets[i].data; for (var j=0; j<data.length; j++) { var value = data[j][k]; if (values.indexOf(value) == -1) { values.push(value); } } } __datasets__.values.push(values); } // sort values for (var i=0; i<__datasets__.values.length; i++) { __datasets__.values[i] = __datasets__.values[i].sort(function(a, b) { if (a < b) return -1; if (a > b) return 1; return 0; }); } // __datasets__.list = datasets; return __this__; }; this.axisX = function(label, grads) { var extrema = plottingData.extrema; var valuesX = __datasets__.values[0]; var valuesY = __datasets__.values[1]; // main components __this__.line( scales[0].min, valuesY[0], scales[0].max, valuesY[0], {size:1, color:'#000'} ); __this__.text( scales[0].max - .0125*scales[0].span, valuesY[0] + .025*scales[1].span, label, {align:'right', size:1, color:'#000'} ); // graduations for (var i=0; i<grads.length; i++) { var opacity = Math.pow(.5, i+1); var grad = grads[i]; // extrema var min = valuesX[0]; min -= min % grad; if (min < valuesX[0]) { min += grad; } var max = valuesX[valuesX.length - 1]; max -= max % grad; while (max < valuesX[valuesX.length - 1]) { max += grad; } // draw for (var x=min; x<max; x+=grad) { __this__.line( x, valuesY[0] - .0125*scales[1].span, x, valuesY[0] + .0125*scales[1].span, {size:1, color:'rgba(0,0,0,' + (2*opacity) + ')'} ); __this__.line( x, valuesY[valuesY.length - 1], x, valuesY[0], {size:1, color:'rgba(0,0,0,' + opacity + ')'} ); if (i == 0) { __this__.text( x, valuesY[0] - .05*scales[1].span, x, {align:'center', size:1, color:'#000'} ); } } } }; this.axisY = function(label, grads, extrema) { extrema = extrema || {}; var valuesX = __datasets__.values[0]; var valuesY = __datasets__.values[1]; var Xmin = (extrema.Xmin != undefined) ? extrema.Xmin : valuesX[0]; var Xmax = (extrema.Xmax != undefined) ? extrema.Xmax : valuesX[valuesX.length - 1]; var Ymin = (extrema.Ymin != undefined) ? extrema.Ymin : valuesY[0]; var Ymax = (extrema.Ymax != undefined) ? extrema.Ymax : valuesY[valuesY.length - 1]; // main components __this__.line( Xmin, scales[1].min, Xmin, scales[1].max, {size:1, color:'#000'} ); __this__.text( Xmin + .0125*scales[0].span, scales[1].max - .04*scales[1].span, label, {align:'left', size:1, color:'#000'} ); // graduations for (var i=0; i<grads.length; i++) { var opacity = Math.pow(.3, i+1); var grad = grads[i]; // extrema var min = Ymin; min -= min % grad; if (min < valuesY[0]) { min += grad; } var max = Ymax + grads[grads.length - 1]; // draw for (var y=min; y<max; y+=grad) { __this__.line( Xmin - .0125*scales[0].span, y, Xmin + .0125*scales[0].span, y, {size:1, color:'rgba(0,0,0,' + (2*opacity) + ')'} ); __this__.line( Xmin, y, Xmax, y, {size:1, color:'rgba(0,0,0,' + opacity + ')'} ); if (i == 0) { var m = Math.max(Ymax/grad, -Ymin/grad); var precision = Math.log(m) / Math.log(10); precision = Math.ceil(precision) __this__.text( Xmin - .02*scales[0].span, y, y.toPrecision(precision), {align:'right', size:1, color:'#000'} ); } } } }; this.viewHistogram = function(labels, options) {// draw the things! // compute average & std var statistics = []; var min = 0; var max = 0; for (var i=0; i<__datasets__.list.length; i++) { var dataset = __datasets__.list[i]; var options = dataset.options; var previousPoint = dataset.data[0]; // compute average var average = 0; var k = __datasets__.dimensions - 1; for (var j=0; j<dataset.data.length; j++) { average += dataset.data[j][k]; } average /= dataset.data.length; // compute standard deviation var k = __datasets__.dimensions - 1; var std = 0; for (var j=0; j<dataset.data.length; j++) { var value = average - dataset.data[j][k]; std += value * value; } std = Math.sqrt(std); // store it for later statistics.push({ average: average, std: std }); var d1 = average - std; var d2 = average + std; if (average < 0) { min = Math.min(d1, min); } else { max = Math.max(d2, max); } } // compute the scales var span = max - min; scales = [{ type: 'numeric', min: -.5, max: __datasets__.list.length + 1, span: __datasets__.list.length + 1.5 }, { type: 'numeric', min: min - .1*span, max: max + .1*span, span: 1.2 * span }]; // graphs for (var i=0; i<__datasets__.list.length; i++) { var dataset = __datasets__.list[i]; var average = statistics[i].average; var options = dataset.options; var std = statistics[i].std; __this__.rect( i + .7, 0, i + 1.3, average, options ); var y = (average>0) ? (average+std) : (average-std); __this__.line( i + 1, 0, i + 1, y, options ); __this__.line( i + .9, y, i + 1.1, y, options ); } // X axis this.line(0, min, __datasets__.list.length + .5, min, {size:1, color:'rgba(0,0,0,.75)'}); // Y axis var values = __datasets__.values[1]; var grad = Math.log(max - min); grad /= Math.log(10); grad = Math.floor(grad); grad = Math.pow(10, grad); __this__.axisY(labels[1], [grad, .1*grad], { Xmin: 0, Xmax: __datasets__.list.length + .5, Ymin: min, Ymax: max, }); }; this.viewLine = function(labels, options) { // compute the scales scales = []; for (var i=0; i<__datasets__.dimensions; i++) { var values = __datasets__.values[i]; if (__datasets__.types[i] == 'number') { var min = values[0]; var max = values[values.length - 1]; var span = max - min; scales.push({ type: 'numeric', min: min - .1 * span, max: max + .1 * span, span: 1.2 * span }); } else { var positions = {}; for (var j=0; j<values.length; j++) { positions[values[j]] = 0; } scales.push({ type: 'discrete', positions: positions }); } } // draw the things! for (var i=0; i<__datasets__.list.length; i++) { var dataset = __datasets__.list[i]; var options = dataset.options; var previousPoint = dataset.data[0]; for (var j=1; j<dataset.data.length; j++) { var point = dataset.data[j]; __this__.line( previousPoint[0], previousPoint[1], point[0], point[1], options ); previousPoint = point; } } // X axis var values = __datasets__.values[0]; var grad = values[values.length-1] - values[0]; grad = Math.log(grad) / Math.log(10); grad = Math.floor(grad); grad = Math.pow(10, grad); __this__.axisX(labels[0], [grad, .1*grad]); // Y axis var values = __datasets__.values[1]; var grad = Math.log(values[values.length-1] - values[0]); grad /= Math.log(10); grad = Math.floor(grad); grad = Math.pow(10, grad); __this__.axisY(labels[1], [grad, .1*grad]); }; this.view = function(name, labels, options) { name = name .toLowerCase() .replace(/s+$/, '') .replace(/^\w/, function(match){return match.toUpperCase()}); __this__.clear(); __this__['view' + name](labels); return __this__; }; (function() { var canvas = document.createElement('canvas'); var canvasContext = canvas.getContext && canvas.getContext('2d'); if (!!canvasContext) { container.appendChild(canvas); contextType = 'canvas'; context = canvasContext; } else { contextType = 'raphael'; context = Raphael(container, width, height); } for (var key in __methods__[contextType]) { __this__['_' + key] = __methods__[contextType][key]; } __this__.size(width, height); __this__.clear(); })(); };