/* graph.js - Paul Seamons - License BSD - Copyright 2010 */
/* $Id: canvas_graph.js,v 1.15 2010-03-24 01:01:31 paul Exp $ */

var CanvasGraph = function (args) {
  var g = this;
  g.bar_width   = .6;
  g.background  = [0,'#bbb',.7,'#fff'];
  g.box_width   = 2;
  g.color       = '#777';
  g.canvas_color= '#fff';
  g.font        = '10px sans-serif';
  g.padding     = 10;
  g.tick_length = 4;
  for (var i in args) g[i] = args[i];
  for(var i=1;i<arguments.length;i++) if(arguments[i]) for(var j in arguments[i]) g[j]=arguments[i][j];

  var d = document;
  if (!g.canvas_id) {
    if (! d._graph_id) d._graph_id = 0;
    g.canvas_id = 'graph_'+(d._graph_id++);
  }
  if (! (g.canvas = d.getElementById(g.canvas_id))) {
    var w = args.width  ? args.width  : args.height ? parseInt(args.height * 5/3) : 500;
    var h = args.height ? args.height : (''+w).match(/^\d+$/) ? parseInt(w * 3/5) : 300;
    d.write('<canvas id="'+g.canvas_id+'" class="graph" style="width:'+w+';height:'+h+'"></canvas>');
    g.canvas = d.getElementById(g.canvas_id);
    g.canvas.style.border = this.border || '1px solid #aaa'
  }

  var _attach = function (el, evname, func, capture) {
    if (el.addEventListener) return el.addEventListener(evname, func, capture ? true : false);
    if (el.attachEvent)      return el.attachEvent('on'+evname, func);
  }
  var _detach = function (el, evname, func, capture) {
    if (el.removeEventListener) return el.removeEventListener(evname, func, capture ? true : false);
    if (el.detachEvent)         return el.detachEvent('on'+evname, func);
  }
  _attach(window, 'load', args.load || function () { g.draw(1) }, true);

  // handle resizing
  var handle_resize = function (e) { g.draw(1) };
  var handle_up = function (e) {
    delete g.mouse_down;
    _detach(document, 'mouseup',   arguments.callee,   true);
    _detach(document, 'mousemove', handle_move, true);
  };
  var handle_move = function (e) {
    g.handleMove(e);
    if (g.mouse_down) {
      g.canvas.style.width  = g.mouse_down.w + e.clientX - g.mouse_down.x;
      g.canvas.style.height = g.mouse_down.h + e.clientY - g.mouse_down.y;
      _detach(window, 'resize', handle_resize, false);
      if (g.drawn) g.draw(1);
    }
    return true;
  };
  _attach(g.canvas, 'mousemove', handle_move, true);
  _attach(g.canvas, 'mousedown', function (e) {
    if (!e) e = window.event;
    g.mouse_down = {x: e.clientX, y: e.clientY, w: g.canvas.clientWidth, h: g.canvas.clientHeight};
    _attach(document, 'mouseup',   handle_up, true);
    _attach(document, 'mousemove', handle_move, true);
  }, true);
  if (args.width && (''+args.width).match(/^\d+%$/)) _attach(window, 'resize', handle_resize, false);
  if (!args.no_interact) _attach(g.canvas, 'mouseout',  function (e) { delete g.hi; g.draw() },  true);
};

if (!CanvasGraph.prototype.draw) (function(){

//  var x = -(document.body.scrollLeft + document.documentElement.scrollLeft);
var get_xy = function (el) {
  if (! el.offsetParent || el.tagName == 'BODY') return {x:-document.body.scrollLeft,y:-document.body.scrollTop};
  var _xy = get_xy(el.offsetParent);
  return {x: el.offsetLeft + _xy.x, y: el.offsetTop + _xy.y};
};
var cmap = {
    blue:  '#0000FF', darkblue:  '#00008B',
    red:   '#FF0000', darkred:   '#8B0000',
    green: '#008000', darkgreen: '#006400',
    orange:'#FFA500', darkorange:'#FF8C00',
    yellow:'#FFFF00', gold:  '#FFD700',
    purple:'#800080', indigo:'#4B0082',
    black: '#000000', gray:  '#808080', white: '#FFFFFF'
};

CanvasGraph.prototype.cmap = function () { return cmap };

CanvasGraph.prototype.handleMove = function (e) {
  var mmap = this.mousemap; if (!mmap) return;
  var xy = get_xy(this.canvas);
  var x = e.clientX - xy.x;
  var y = e.clientY - xy.y;
  var i,j;
  for (i = 0; i < mmap.length; i++) {
    if (x < mmap[i][1] || x > mmap[i][2]) continue;
    var min, J, ys = mmap[i][3];
    for (var j = 0; j < ys.length; j++) {
      if (typeof(ys[j]) == 'undefined') continue;
      var dy = Math.abs(ys[j] - y);
      if (typeof(min) != 'undefined' && min <= dy) continue;
      min = dy;
      J   = j;
    }
    j = J;
    if (this.hi && (this.hi[0] != i  || this.hi[1] != j)) delete this.hi;
    break;
  }

  if (!this.mouse_down && !this.hi && typeof(i) != 'undefined' && typeof(j) != 'undefined') {
    this.hi = [i,j];
    this.draw();
  }
}

CanvasGraph.prototype.text_metrics = function (s, ctx) {
  var m=(''+ctx.font).match(/^(\d+)px/);
  if (typeof s == 'undefined' || !s.length) return {h:0,w:0};
  return {h: m?parseInt(m[1]):10, w:(s.length&&ctx.measureText)?ctx.measureText(s).width:0};
};

CanvasGraph.prototype.parseFill = function (bg, dim, bound, ctx) {
  if (Function.prototype.isPrototypeOf(bg)) bg = bg(dim, bound, ctx);
  if (!Array.prototype.isPrototypeOf(bg)) return bg;
  var i = 0;
  if (Array.prototype.isPrototypeOf(bg[i])) dim = bg[i++];
  else dim[0] = dim[2] = 0;
  var g = ctx.createLinearGradient.apply(ctx, dim);
  for (;i < bg.length;) g.addColorStop(bg[i++], bg[i++]);
  return g;
}

CanvasGraph.prototype.draw = function (sized) {
  if (this.drawing) return; this.drawing = 1;
  var cv = this.canvas;
  if (sized) {
    cv.width  = cv.clientWidth;
    cv.height = cv.clientHeight;
  }
  var c  = cv.getContext('2d');
  var W  = cv.width;
  var H  = cv.height;

  var p = this.padding;
  var yt = p;
  var yb = H - p - this.tick_length;
  var xl = p;
  var xr = W - p;
  c.font = this.font || '10px sans-serif';
  var R  = this.rotateX || 0;
  if (R) R = R * Math.PI / 180;

  var mm = this.minmax();
  var xlh = 0;
  for (var i = 0; i < this.n_points; i++) {
    var t = this.labels[i];
    if (typeof t == 'undefined') t = '';
    if (typeof t != 'object') this.labels[i] = t = {str: ''+t};
    if (typeof t.w == 'undefined') {
      var m = this.text_metrics(t.str,c);
      t.w = m.w;
      t.h = m.h;
    }
    var h = R ? (Math.abs(Math.cos(R)*t.h) + Math.abs(Math.sin(R)*t.w)) : t.h;
    if (xlh < h) xlh = h;
  }

  var mmap = this.mousemap = [];

  c.fillStyle = this.parseFill(this.canvas_color,[0,0,W,H],[0,0,W,H],c);
  c.fillRect(0,0,W,H);

  // title and x labels
  var m = this.text_metrics(this.title||'',c);
  if (m.w) {
    var s = this.title_scale || 1.6;
    c.save();
    c.fillStyle = this.title_color || this.text_color || this.color;
    c.translate((xr+xl-m.w*s)/2,yt+m.h*s/2);
    c.scale(s,s);
    c.textBaseline = 'middle';
    c.fillText(this.title,0,0);
    c.restore();
    yt += m.h*s + p;
  }
  m = this.text_metrics(this.title_y||'',c);
  if (m.w) {
    var s = this.title_y_scale || 1.3;
    c.save();
    c.fillStyle = this.title_y_color || this.text_color || this.color;
    c.translate(xl+m.h*s/2,(yb+yt+m.w*s)/2);
    c.scale(s,s);
    c.rotate(-Math.PI/2);
    c.textBaseline = 'middle';
    c.fillText(this.title_y,0,0);
    c.restore();
    xl += m.h*s + p;
  }
  m = this.text_metrics(this.title_x||'',c);
  if (m.w) {
    var s = this.title_x_scale || 1.3;
    c.save();
    c.fillStyle = this.title_x_color || this.text_color || this.color;
    c.translate((xr+xl-m.w*s)/2,yb-m.h*s/2);
    c.scale(s,s);
    c.textBaseline = 'middle';
    c.fillText(this.title_x,0,0);
    c.restore();
    yb -= (m.h*s + p);
  }
  yb -= xlh;

  // figure out bounds of the data
  var y_min = (typeof this.y_min != 'undefined') ? this.y_min : mm.min;
  var y_max = this.y_max;
  var inc;
  if (typeof y_max == 'undefined') {
    inc = 1 * Math.pow(10, ((""+parseInt(mm.max)).length - 1));
    if (Math.abs(mm.max - inc) < .0000001)
      y_max = mm.max;
    else
      y_max = (1 + parseInt(mm.max / inc)) * inc;
  }
  if (! this.y_min_zoom && y_min > 0) y_min = 0;
  var y_px  = (yb - yt) / (y_max - y_min);
  var yzero = yb;
  if (y_min < 0) yzero += y_px * y_min;

  var I = y_max / (inc * .25) * 5;
  while (I > 10) I /= 2;
  //while (I < 10) I *= 2;

  // y axis labels
  var N = [[0,yzero,0]];
  if (y_min < 0) {
    while (N[0][0] > y_min) {
      var n = N[0][0] - y_max/I;
      if (n <= y_min) break;
      N.unshift([n, yzero-y_px*n,0]);
    }
    N.unshift([y_min,yb,0]);
  }
  for (var i = 1; i < I; i++) {
    var n = i * y_max / I;
    var y = (yb - y_px*(n - y_min));
    N.push([n,y,0]);
  }
  N.push([y_max, yt, 0]);
  var tmax = -1;
  for (var i = 0; i < N.length; i++) {
    N[i][0] = ''+N[i][0].toFixed(2);
    N[i][2] = this.text_metrics(N[i][0],c).w;
    if (tmax < N[i][2]) tmax = N[i][2];
  }
  if (tmax > 0) {
    c.save();
    c.fillStyle = this.axis_color_y || this.text_color || this.color;
    c.textBaseline = 'middle';
    for (var i = 0; i < N.length; i++) {
      if (!document.first) {
        document.first = 1;
      }
      if (N[i][2]) c.fillText(N[i][0], xl + (tmax - N[i][2]), N[i][1]);
    }
    xl += tmax + 4;
    c.restore();
  }
  xl += this.tick_length;

  // x axis
  var brdw = this.box_width;
  var barw = (xr - xl) / this.n_points;
  c.strokeStyle = this.axis_color_x || this.axis_color || this.box_color || this.color;
  c.save();
  c.beginPath();
  c.lineWidth = brdw * .5;
  for (var j = 0; j < this.n_points; j++) {
    var x = xl + (barw / 2) + j * barw;
    c.moveTo(x, yb+brdw/2);
    c.lineTo(x, yb+brdw/2+this.tick_length);
    mmap[j] = [x,x-barw/2,x+barw/2, []];
  }
  c.stroke();
  c.restore();
  c.save();
  c.beginPath();
  c.lineWidth = brdw;
  c.moveTo(xl-brdw/2,yb); c.lineTo(xr+brdw/2,yb);
  c.stroke();
  c.closePath();
  c.restore();
  c.fillStyle = this.axis_color_x || this.text_color || this.color;
  c.textBaseline = 'bottom';
  for (var j = 0; j < this.n_points; j++) {
    var a = this.labels[j];
    var s = a.str; if (typeof s == 'undefined' || !s.length) continue;
    var x = xl + (barw / 2) + j * barw;
    if (R > 0) { c.save(); c.translate(x-Math.sin(R)*a.h/2, yb+Math.cos(R)*a.h+brdw+this.tick_length); c.rotate(R); c.fillText(s, 0, 0); c.restore() }
    else if (R < 0) { c.save(); c.translate(x-Math.sin(R)*a.h/2, yb+Math.cos(R)*a.h+brdw+this.tick_length); c.rotate(R); c.fillText(s, -a.w, 0); c.restore() }
    else c.fillText(s, x - (a.w/2), yb+(a.h/2)+brdw+this.tick_length);
  }

  // y axis
  c.strokeStyle = this.axis_color_y || this.axis_color || this.box_color || this.color;
  c.save();
  c.beginPath();
  c.lineWidth = brdw * .5;
  for (var i = 0; i < N.length; i++) {
    c.moveTo(xl-brdw/2, N[i][1]);
    c.lineTo(xl-brdw/2-this.tick_length, N[i][1]);
  }
  c.stroke();
  c.closePath();
  c.beginPath();
  c.lineWidth = brdw;
  c.moveTo(xl,yt-brdw/2); c.lineTo(xl, yb+brdw/2);
  c.closePath();
  c.stroke();
  c.restore();

  // bounding box
  c.save();
  c.beginPath();
  c.lineWidth = brdw;
  c.moveTo(xl,yt); c.lineTo(xr,yt); c.lineTo(xr, yb);
  c.strokeStyle = this.box_color || this.color;
  c.stroke();
  c.restore();
  xl += brdw/2;
  xr -= brdw/2;
  yt += brdw/2;
  yb -= brdw/2;

  // background
  c.save();
  c.beginPath();
  c.moveTo(xl,yt); c.lineTo(xr,yt); c.lineTo(xr, yb); c.lineTo(xl, yb);
  c.fillStyle = this.parseFill(this.background,[xl,yt,xr,yb],[xl,yt,xr,yb],c);
  c.fill();
  c.closePath();
  c.restore();

  // lines in the back
  c.save(); c.beginPath();
  c.lineWidth = brdw * .5;
  for (var i = 1; i < N.length - 1; i++) {
    c.moveTo(xl, N[i][1]);
    c.lineTo(xr, N[i][1]);
  }
  c.strokeStyle = this.bar_color_y || this.bar_color || this.box_color || this.color;
  c.stroke(); c.restore();

  // now for the data
  for (var i = 0; i < this.series.length; i++) {
    var s = this.series[i];
    c.save();
    c.beginPath();
    var first = undefined;
    var last;
    var points = [];
    for (var j = 0; j < this.n_points; j++) {
      var n = s.data[j];
      if (typeof n == 'undefined') continue;
      var x = last = xl + (barw / 2) + j * barw;
      var y = (yb - y_px*(n - y_min));
      mmap[j][3][i] = y;
      if (s.bar) {
        var x1 = x - (barw * this.bar_width) / 2;
        var x2 = x + (barw * this.bar_width) / 2;
        c.moveTo(x1, yzero);
        c.lineTo(x1, y);
        c.lineTo(x2, y);
        c.lineTo(x2, yzero);
        c.lineTo(x1, yzero);
      } else if (typeof first == 'undefined') {
        first = x;
        c.moveTo(x, y);
      } else
        c.lineTo(x, y);
      if (s.point) points.push([x,y,n]);
    }
    c.fillStyle = this.parseFill(s.fillStyle || s.color,[xl,yt,xr,yb],[xl,yt,xr,yb],c);
    if (s.bar) {
      c.closePath();
      c.fill();
    } else if (s.fill) {
      c.lineTo(last, yzero);
      c.lineTo(first,yzero);
      c.closePath();
      c.fill();
    }
    c.lineWidth   = s.linewidth || 1.5;
    c.strokeStyle = s.color;
    c.stroke();
    if (points.length) for (var k = 0; k < points.length; k++) {
      c.beginPath();
      c.arc(points[k][0], points[k][1], s.point/2, 0, Math.PI*2, 0);
      c.fillStyle = this.parseFill(s.fillStyle || s.color,[x1,y,x2,yzero],[xl,yt,xr,yb],c);
      c.fill();
      c.stroke();
      c.closePath();
    }
    c.restore();
  }

  if (this.hi) {
    var i = this.hi[0];
    var j = this.hi[1];
    var x = this.mousemap[i][0];
    var y = this.mousemap[i][3][j];
    var t = ''+this.series[j].data[i];
    var m = this.text_metrics(t,c);
    var w = m.w; if (w < 20) w = 20;
    var r = 5;
    c.save();
    c.beginPath();
    if (!this.series[j].point) c.moveTo(x,y);
    else c.arc(x,y, this.series[j].point/2+3, -Math.PI*.40, -Math.PI*.60, 0);
    c.lineTo(x-5, y-15);
    var X = x-w/2, Y = y-15;
    c.lineTo(X,Y); X -= r;
    c.bezierCurveTo(X+r/2,Y, X,Y-r/2, X,Y-r); Y-=r;
    c.lineTo(X,Y-m.h); Y-=m.h+r;;
    c.bezierCurveTo(X,Y+r/2, X+r/2,Y, X+r,Y); X=x+w/2;
    c.lineTo(X,Y); X += r;
    c.bezierCurveTo(X-r/2,Y, X,Y+r/2, X,Y+r); Y+=r+m.h;
    c.lineTo(X,Y); Y += r;
    c.bezierCurveTo(X,Y-r/2, X-r/2,Y, X-r,Y);
    c.lineTo(x+5,y-15);
    c.closePath();
    var g = c.createLinearGradient(0,y-15-m.h-2*r,0,y-8);
    g.addColorStop(.6,'rgba(190,190,190,.8)');
    g.addColorStop(1,'rgba(190,190,190,.3)');
    c.fillStyle = g;
    c.fill();
    c.strokeStyle = 'black';
    c.stroke();
    c.restore();
    c.save();
    c.fillStyle = 'black';
    c.textBaseline = 'middle';
    c.fillText(t,x-m.w/2, y-15-m.h/2-r);
    c.restore();
  }
  this.drawn = 1;
  delete this.drawing;
};

// calculate our bounds
CanvasGraph.prototype.minmax = function () {
  if (typeof this.max == 'undefined') {
    if (this.check_data()) return;
    delete this.min;
    for (var i = 0; i < this.series.length; i++) {
      for (var j = 0; j < this.n_points; j++) {
        var n = this.series[i].data[j];
        if (typeof n == 'undefined') continue;
        if (typeof this.min == 'undefined' || this.min > n) this.min = n;
        if (typeof this.max == 'undefined' || this.max < n) this.max = n;
      }
    }
  }
  return {'min': this.min||0, 'max': this.max||0};
};

// make sure all data is in the correct layout
CanvasGraph.prototype.check_data = function () {
  if (! this.labels) this.labels = [];
  if (! this.series) this.series = [];
  if (! this.series.length) {
    if (! this.data) { alert("Missing data or series"); return }
    this.series[0] = {'data': this.data};
  }
  if (! this.n_points) {
    this.n_points = this.labels.length;
    for (var i = 0; i < this.series.length; i++) if (this.n_points < this.series[i].data.length) this.n_points = this.series[i].data.length;
  }
  if (this.labels.length < this.n_points) this.labels.length = this.n_points;
  var c = this.colors    || ['darkblue', 'darkgreen', 'indigo', 'darkred', 'darkorange', 'gold', 'gray', 'black', 'blue', 'purple', 'green', 'red'];
  var l = this.linetypes || ['dashed', 'solid'];
  for (var i = 0; i < this.series.length; i++) {
    var s = this.series[i];
    if (this.n_points < s.data.length) this.n_points = s.data.length;
    var n = parseInt(i / c.length);
    if (! s.linetype) s.linetype = l[n];
    if (! s.color) s.color = c[i - n * c.length];
    s.color = cmap[s.color] || s.color;
    var m;
    if (! s.fillStyle) s.fillStyle
      = (m = s.color.match(/^rgba/)) ? s.color
      : (m = s.color.match(/^\#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/i)) ? 'rgba('+parseInt(m[1],16)+','+parseInt(m[2],16)+','+parseInt(m[3],16)+','+(s.alpha||.4)+')'
      : (m = s.color.match(/^rgb\((.+)\)$/)) ? 'rgba('+m[1]+','+(s.alpha||.4)+')'
      : '';
  }
};

CanvasGraph.prototype.add = function () {
  this.check_data();
  this.labels.push(arguments[0]);
  var n = this.labels.length - this.n_points; if (n < 0) n = 0;
  for (var j = 0; j < n; j++) this.labels.shift();
  for (var i = 0; i < this.series.length; i++) {
    this.series[i].data.push(arguments[i+1]);
    for (var j = 0; j < n; j++) this.series[i].data.shift();
  }
  delete this.max;
  delete this.min;
  if (this.drawn) this.draw();
};

})();
