////////////////////////////////////////////////////////////////////////////////
//Filename: svg_graph.js
//Purpose:  Creates and populates an SVG graph based on value/timestamp tuples
//          retrieved via ajax.
//Author:   Michael Kirkland
//Date:     March, 2009
//License:
//Copyright (C) 2009 Michael Kirkland
//
//This library is free software; you can redistribute it and/or
//modify it under the terms of the GNU Lesser General Public
//License as published by the Free Software Foundation; either
//version 2.1 of the License, or (at your option) any later version.
//
//This library is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//Lesser General Public License for more details.
//
//You should have received a copy of the GNU Lesser General Public
//License along with this library; if not, write to the Free Software
//Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
//
//Contact:  svg_graph@michaelkirkland.org
//Website:  http://michaelkirkland.org/svg_graph
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
//Function: constructor
//Purpose:  Defines member variables
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph = function()
{
  this.ns = 'http://www.w3.org/2000/svg';
  this.svg = null;
  this.width = 800;
  this.height = 300;
  this.graph_height = null;
  this.graph_width = null;
  this.max_x = null;
  this.min_x = null;
  this.max_y = null;
  this.min_y = null;
  this.spacing = 40;
  this.sigdigits = 1;
  this.ylabel_left = true;
  this.ylabel_right = true;
  this.xticks = 'monthly';

  this.points = [];
  this.dates = [];
  this.labels = [];
};

////////////////////////////////////////////////////////////////////////////////
//Function: setup
//Purpose:  Creates a new graph object and attaches it to the div specified.
//Arguments
//div_id:   Id of the div to attach to.
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.setup = function(params)
{
  var g = new svg_graph();
  var div = null;
  if(params['div'])
  {
    div = document.getElementById(params['div']);
  }
  else
    return null;
  g.attach(div, params);

  return g;
};

////////////////////////////////////////////////////////////////////////////////
//Function: attach
//Purpose:  Initializes a new graph object and attaches it to the div object
//          passed.
//Arguments
//div:      div object to attach to
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.attach = function(div, params)
{
  parent = div;
  div.svg_graph = this;

  for(i in params)
  {
    switch(i)
    {
      case 'width':
      {
	this.width = params[i];
	break;
      }
      case 'height':
      {
	this.height = params[i];
	break;
      }
      case 'sigdigits':
      {
	this.sigdigits = params[i];
	break;
      }
      case 'ylabel':
      {
	if(params[i] == 'left')
	{
	  this.ylabel_left = true;
	  this.ylabel_right = false;
	}
	else if(params[i] == 'right')
	{
	  this.ylabel_left = false;
	  this.ylabel_right = true;
	}
	else if(params[i] == 'both')
	{
	  this.ylabel_left = true;
	  this.ylabel_right = true;
	}
	break;
      }
      case 'xticks':
      {
	this.xticks = params[i];
	break;
      }
    }
  }
 
  this.graph_height = this.height - 100; //100 = space for legend
  //space for tick labels hardcoded to 40 for now
  this.graph_width = this.width -
                     (this.ylabel_left ? 40 : 0) -
		     (this.ylabel_right ? 40 : 0);
  this.svg = this.build_bg();

  div.appendChild(this.svg);
  div.style.width = this.width + 'px';
  div.style.height = this.height + 'px';
};

////////////////////////////////////////////////////////////////////////////////
//Function: build_bg
//Purpose:  Initializes an SVG object and draws rectangles to fill the
//          background.
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.build_bg = function()
{
  var svg = document.createElementNS(this.ns, 'svg');
  svg.setAttribute('xmlns', this.ns);
  svg.setAttribute('width', this.width);
  svg.setAttribute('height', this.height);
  svg.setAttribute('viewBox', '0 0 ' + this.width + ' ' + this.height);

  var svg_bg = document.createElementNS(this.ns, 'rect');
  svg_bg.setAttribute('class', 'svg_background');
  svg_bg.setAttribute('x', '0');
  svg_bg.setAttribute('y', '0');
  svg_bg.setAttribute('width', this.width);
  svg_bg.setAttribute('height', this.height);
  svg.appendChild(svg_bg);

  var graph_bg = document.createElementNS(this.ns, 'rect');
  graph_bg.setAttribute('class', 'graph_background');
  graph_bg.setAttribute('x', (this.ylabel_left ? 40 : 0))
  graph_bg.setAttribute('y', '0');
  graph_bg.setAttribute('width', this.graph_width);
  graph_bg.setAttribute('height', this.graph_height);
  svg.appendChild(graph_bg);

  return svg;
};

////////////////////////////////////////////////////////////////////////////////
//Function: build_legend
//Purpose:  Builds a legend of the plots contained in a graph, with squares of
//          colour and a short label.
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.build_legend = function()
{
  group = document.createElementNS(this.ns, 'g');
  this.svg.appendChild(group);
  var start_x = 10;
  var start_y = this.graph_height + 30;
  var x = start_x;
  var y = start_y;
  var longest_label = 0;

  for(i in this.points)
  {
    if(y + 15 > this.height)
    {
      y = start_y;
      //longest label + color rect width + its 5px buffer + 10 px of buffer
      x += longest_label + 20 + 5 + 10;
    }
    rect = document.createElementNS(this.ns, 'rect');
    rect.setAttribute('x', x);
    rect.setAttribute('y', y);
    rect.setAttribute('width', 20);
    rect.setAttribute('height', 20);
    rect.setAttribute('class', 'fill' + i);
    group.appendChild(rect);

    label = document.createElementNS(this.ns, 'text');
    label.setAttribute('x', x + 25);
    label.setAttribute('y', y + 15);
    label.appendChild(document.createTextNode(this.labels[i]));
    group.appendChild(label);
    longest_label = label.getComputedTextLength() > longest_label ? 
                    label.getComputedTextLength() : longest_label;

    y += 25;
  }
};

////////////////////////////////////////////////////////////////////////////////
//Function: build_grid
//Purpose:  Builds a series of guidelines based on the y axis of the graph we're
//          building.
//Arguments
//svg:      The SVG object to attach the guildlines to
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.build_grid = function()
{
  var num_lines = parseInt(this.graph_height/this.spacing);
  var increment = (this.max_y - this.min_y) / num_lines;
  for(var i = num_lines;i >= 0;i--)
  {
    var path = document.createElementNS(this.ns, 'path');
    path.setAttribute('class', 'guideline');
    path.setAttribute('d', 'M0 ' + (this.graph_height - this.spacing * i) +
                      ' h' + this.width);
    this.svg.appendChild(path);

    var label_left = document.createElementNS(this.ns, 'text');
    var label_right = document.createElementNS(this.ns, 'text');
    label_left.setAttribute('class', 'ytick_label');
    label_right.setAttribute('class', 'ytick_label');
    if(this.ylabel_left)
      this.svg.appendChild(label_left);
    if(this.ylabel_right)
      this.svg.appendChild(label_right);

    var tick_value = (i * increment) + this.min_y;
    var text_left = document.createTextNode(tick_value.toFixed(this.sigdigits));
    var text_right =
      document.createTextNode(tick_value.toFixed(this.sigdigits));
    label_left.appendChild(text_right);
    label_right.appendChild(text_left);

    label_left.setAttribute('x', 0);
    label_left.setAttribute('y', this.graph_height - (this.spacing * i) + 15);
    label_right.setAttribute('x',
                             this.width - label_right.getComputedTextLength());
    label_right.setAttribute('y', this.graph_height - (this.spacing * i) + 15);
  }

  var start = new Date(this.min_x * 1000);
  var end = new Date(this.max_x * 1000);
  switch(this.xticks)
  {
    case 'hourly':
    {
      this.hour_ticks(start, end);
      break;
    }
    case 'daily':
    {
      this.day_ticks(start, end);
      break;
    }
    case 'monthly':
    {
      this.month_ticks(start, end);
      break;
    }
    case 'yearly':
    {
      this.year_ticks(start, end);
      break;
    }
  }
};

////////////////////////////////////////////////////////////////////////////////
//Function: hour_ticks
//Purpose:  Builds hourly ticks on the x axis.
//Arguments
//start:    Javascript Date object representing the lower bound of the graph.
//end:      Javascript Date object representing the upper bound of the graph.
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.hour_ticks = function(start, end)
{
  var first_tick = new Date(start.getFullYear(),
                            start.getMonth(),
			    start.getDate(),
			    start.getHours() + 1);
  var curtick = parseInt(first_tick.getTime()/1000);
  var cutoff = parseInt(end.getTime()/1000);

  while(curtick <= cutoff)
  {
    var tick = document.createElementNS(this.ns, 'path');

    var tick_pos = this.translate_x(curtick) + (this.ylable_left ? 40 : 0);
    tick.setAttribute('d', 'M' + tick_pos + ' ' + this.graph_height +
                           'L' + tick_pos + ' ' + (this.graph_height + 10));
    tick.setAttribute('class', 'guideline');
    this.svg.appendChild(tick);

    var label = document.createElementNS(this.ns, 'text');
    this.svg.appendChild(label);
    var time = new Date(curtick * 1000);
    var curhour = time.getHours() >= 10 ?
      time.getHours() : '0' + time.getHours();
    var curminute = time.getMinutes() >= 10 ?
      time.getMinutes() : '0' + time.getMinutes();
    var text = document.createTextNode(curhour + ':' + curminute);
    label.appendChild(text);
    label.setAttribute('x', tick_pos - 23);
    label.setAttribute('y', this.graph_height + 30)

    curtick += 3600;
  }
};

////////////////////////////////////////////////////////////////////////////////
//Function: day_ticks
//Purpose:  Builds daily ticks on the x axis.
//Arguments
//start:    Javascript Date object representing the lower bound of the graph.
//end:      Javascript Date object representing the upper bound of the graph.
//Note:     Unimplemented stub. This will need to handle month rollovers and
//          leap years.
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.day_ticks = function()
{
};

////////////////////////////////////////////////////////////////////////////////
//Function: month_ticks
//Purpose:  Builds monthly ticks on the x axis.
//Arguments
//start:    Javascript Date object representing the lower bound of the graph.
//end:      Javascript Date object representing the upper bound of the graph.
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.month_ticks = function(start, end)
{
  var months = 0;
  if(end.getYear() - start.getYear() == 0)
  {
    months = end.getMonth() - start.getMonth();
  }
  else
  {
    months += 12 - start.getMonth();
    months += 12 * (end.getYear() - start.getYear() - 1);
    months += end.getMonth();
  }
  //Unless our first data point happens to be on the first day of a month, we
  //want the first tick to be the next month.
  if(start.getDay() != 1)
  {
    months--;
    start_month = start.getMonth() + 1;
  }
  else
  {
    start_month = start.getMonth();
  }

  //Set ticks on the x axis at the first of each month for which we have data.
  var month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                     'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  for(var i = 0;i<=months;i++)
  {
    var curdate = new Date(start.getFullYear() + 
                           parseInt((i + start_month)/12),
                           (i + start_month) % 12, 1);
    var tick_lat = this.translate_x(curdate.getTime() / 1000);
    tick = document.createElementNS(this.ns, 'path');
    tick.setAttribute('class', 'guideline');
    tick.setAttribute('d', 'M' + tick_lat + ' ' + this.graph_height +
                      ' L' + tick_lat + ' ' + (this.graph_height + 10));
    this.svg.appendChild(tick);
    var label = document.createElementNS(this.ns, 'text');
    label.setAttribute('x', tick_lat - 15);
    label.setAttribute('y', (this.graph_height + 20));
    label.appendChild(
      document.createTextNode(month_names[(i + start_month) % 12]));
    this.svg.appendChild(label);
  }
};

////////////////////////////////////////////////////////////////////////////////
//Function: year_ticks
//Purpose:  Builds yearly ticks on the x axis.
//Arguments
//start:    Javascript Date object representing the lower bound of the graph.
//end:      Javascript Date object representing the upper bound of the graph.
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.year_ticks = function(start, end)
{
  var firstyear = start.getMonth() == 1 && start.getDate() == 1 ?
    start.getFullYear() : start.getFullYear() + 1;
  var lastyear = end.getFullYear();

  for(var i = firstyear; i <= lastyear; i++)
  {
    var curdate = new Date(i, 1, 1);
    var tick_lat = this.translate_x(curdate.getTime()/1000)
    tick = document.createElementNS(this.ns, 'path');
    tick.setAttribute('class', 'guideline');
    tick.setAttribute('d', 'M' + tick_lat + ' ' + this.graph_height + ' ' +
                           'L' + tick_lat + ' ' + (this.graph_height + 10));
    this.svg.appendChild(tick);

    var label = document.createElementNS(this.ns, 'text');
    label.appendChild(document.createTextNode(i));
    this.svg.appendChild(label);
    label.setAttribute('x', tick_lat - label.getComputedTextLength()/2);
    label.setAttribute('y', (this.graph_height + 30));
 }
};

////////////////////////////////////////////////////////////////////////////////
//Function: add_series
//Purpose:  Adds a plot to be graphed and updates the graph's upper and lower
//          bounds.
//Arguments
//points:   x axis values to plot
//dates:    Dates in seconds since epoch to plot on the y axis
//label:    String to use as a label in the legend.
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.add_series = function(points, dates, label)
{
  max_y = Math.max.apply(Math, points);
  min_y = Math.min.apply(Math, points);
  max_x = Math.max.apply(Math, dates);
  min_x = Math.min.apply(Math, dates);

  this.max_y = this.max_y == null || this.max_y < max_y ? max_y : this.max_y;
  this.min_y = this.min_y == null || this.min_y > min_y ? min_y : this.min_y;
  this.max_x = this.max_x == null ||this.max_x < max_x ? max_x : this.max_x;
  this.min_x = this.min_x == null ||  this.min_x > min_x? min_x : this.min_x;

  this.points.push(points);
  this.dates.push(dates);
  this.labels.push(label);
};

////////////////////////////////////////////////////////////////////////////////
//Function: make_rolling
//Purpose:  Same as add_series, but turns the x axis data points into a 10 point
//          rolling average.
//Arguments
//points:   points to average 
//dates:    Dates in seconds since epoch to plot on the y axis
//label:    String to use as a label in the legend.
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.make_rolling = function(points, dates, label)
{
  var roll = [];
  var mass = 0;
  var average = [];
  var rolling_dates = [];
  for(var i = 0;i < points.length;i++)
  {
    roll.push(points[i]);
    mass += points[i];
    if(i >= 9)
    {
      average.push(mass/10);
      rolling_dates.push(dates[i]);
      mass -= roll.shift();
    }
  }
  this.points.push(average);
  this.dates.push(rolling_dates);
  this.labels.push('Rolling average of ' + label);
};

////////////////////////////////////////////////////////////////////////////////
//Function: draw
//Purpose:  Builds the SVG graph and injects it into our div.
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.draw = function()
{
  var buffer = (this.max_y - this.min_y) * 0.1;
  this.max_y += buffer;
  this.min_y -= buffer;
  this.build_grid(this.svg);

  for(var i = 0;i < this.points.length;i++)
  {
    this.svg.appendChild(this.build_plot(this.points[i], this.dates[i], i));
  }
  this.build_legend();
}

////////////////////////////////////////////////////////////////////////////////
//Function: clear
//Purpose:  Clears previously entered data, but not options.
//Notes:    Unimplemented stub
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.clear = function()
{
}

////////////////////////////////////////////////////////////////////////////////
//Function: translate_y
//Purpose:  Translates a y axis data point into its corresponding pixel in the
//          graph.
//Arguments
//y:        Data point to translate.
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.translate_y = function(y)
{
  return this.graph_height -
         ((this.graph_height / (this.max_y - this.min_y)) * (y - this.min_y));
};

////////////////////////////////////////////////////////////////////////////////
//Function: translate_x
//Purpose:  Translates an x axis data point into its corresponding pixel in the
//          graph
//Arguments
//x:        Data point to translate.
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.translate_x = function(x)
{
  return (this.ylabel_left ? 40 : 0) + //40 = space for the y axis tick labels
         ((this.graph_width / (this.max_x - this.min_x)) * (x - this.min_x));
};

////////////////////////////////////////////////////////////////////////////////
//Function: build_plot
//Purpose:  Builds a plot line based on the data in an array.
//Arguments
//points:   Points to plot
//Author:   Michael Kirkland
//Date:     March, 2009
////////////////////////////////////////////////////////////////////////////////
svg_graph.prototype.build_plot = function(points, dates, series)
{
  var group = document.createElementNS(this.ns, 'g');
  var line_str = 'M' + this.translate_x(dates[0]) + ' ' +
                 this.translate_y(points[0]);
//  var increment = 760 / (points.length - 1);
  var path = document.createElementNS(this.ns, 'path');
  for(var i = 1;i < points.length;i++)
  {
    var x = this.translate_x(dates[i]);
    var y = this.translate_y(points[i]);

    line_str += ' L';
    line_str += x + ' ';
//we would use this for a non time based line graph
//    line_str += ((i * increment) + 40) + ' ';
    line_str += y;

    //small dot to display on the line when the mouse hovers nearby
    var point_hover = document.createElementNS(this.ns, 'g');
    point_hover.setAttribute('opacity', 0);
    var dot = document.createElementNS(this.ns, 'circle');
    dot.setAttribute('class', 'dot' + series);
    dot.setAttribute('cx', x);
    dot.setAttribute('cy', y);
    dot.setAttribute('r', 2.5);
    point_hover.appendChild(dot);
    
    //larger circle within which will make the dot display if the mouse is in it
    var mouseover = document.createElementNS(this.ns, 'circle');
    mouseover.setAttribute('class', 'mouseover');
    mouseover.setAttribute('cx', x);
    mouseover.setAttribute('cy', y);
    mouseover.setAttribute('r', 10);
    mouseover.setAttribute('onmouseover',
                           'evt.target.parentNode.setAttribute("opacity",1)');
    mouseover.setAttribute('onmouseout',
                           'evt.target.parentNode.setAttribute("opacity",0)');
    point_hover.appendChild(mouseover);

    //text label
    var label = document.createElementNS(this.ns, 'text');
    label.setAttribute('x', x + 5);
    label.setAttribute('y', y - 5);
    label.appendChild(
      document.createTextNode(points[i].toFixed(this.sigdigits)));
    point_hover.appendChild(label);

    group.appendChild(point_hover);
  }
  path.setAttribute('d', line_str);
  path.setAttribute('class', 'plot' + series);
  group.appendChild(path);
  return group;
};
