/*!
 * Ico
 * Copyright (C) 2009-2011 Alex R. Young
 * MIT Licensed
 */

/**
 * The Ico object.
 */
(function(global) {
  var Ico = {
    VERSION: '0.3.10',

    /**
     * Rounds a float to the specified number of decimal places.
     *
     * @param {Float} num A number to round
     * @param {Integer} dec The number of decimal places
     * @returns {Float} The rounded result
     */
    round: function(num, dec) {
      var result = Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
      return result;
    }
  };

/**
 * Determines if a value is valid as a 'step' value.
 * Steps are the increments between each bar or line.
 *
 * @param {Integer} value A number to test
 * @returns {Integer} A valid step value
 */
function validStepDivider(value) {
  return value > 1 ? value - 1 : 1;
}

/**
 * Gets a CSS style property.
 *
 * @param {Object} el A DOM element
 * @param {String} styleProp The name of a style property
 * @returns {Object} The style value
 */
function getStyle(el, styleProp) {
  if (typeof window === 'undefined') {
    return;
  }

  var style;
  if (el.currentStyle) {
    style = el.currentStyle[styleProp];
  } else if (window.getComputedStyle) {
    style = document.defaultView.getComputedStyle(el, null).getPropertyValue(styleProp);
  }
  if (style && style.length === 0) {
    style = null;
  }
  return style;
}

var Helpers = {};

Helpers.sum = function(a) {
  var i, sum;
  for (i = 0, sum = 0; i < a.length; sum += a[i++]) {}
  return sum;
};

if (typeof Array.prototype.max === 'undefined') {
  Helpers.max = function(a) {
    return Math.max.apply({}, a);
  };
} else {
  Helpers.max = function(a) {
    return a.max();
  };
}

if (typeof Array.prototype.min === 'undefined') {
  Helpers.min = function(a) {
    return Math.min.apply({}, a);
  };
} else {
  Helpers.min = function(a) {
    return a.min();
  };
}

Helpers.mean = function(a) {
  return Helpers.sum(a) / a.length;
};

Helpers.variance = function(a) {
  var mean = Helpers.mean(a),
      variance = 0;
  for (var i = 0; i < a.length; i++) {
    variance += Math.pow(a[i] - mean, 2);
  }
  return variance / (a.length - 1);
};

Helpers.standard_deviation = function(a) {
  return Math.sqrt(Helpers.variance(a));
};

if (typeof Object.extend === 'undefined') {
  Helpers.extend = function(destination, source) {
    for (var property in source) {
      if (source.hasOwnProperty(property)) {
        destination[property] = source[property];
      }
    }
    return destination;
  };
} else {
  Helpers.extend = Object.extend;
}

if (Object.keys) {
  Helpers.keys = Object.keys;
} else {
  Helpers.keys = function(o) {
    if (o !== Object(o)) {
      throw new TypeError('Object.keys called on non-object');
    }

    var ret = [], p;
    for (p in o) {
      if (Object.prototype.hasOwnProperty.call(o,p)) {
        ret.push(p);
      }
    }
    return ret;
  }
}
/**
 * Normalises lists of values to fit inside a graph.
 *
 * @param {Array} data A list of values
 * @param {Object} options Can be used to set the `start_value`
 */
Ico.Normaliser = function(data, options) {
  this.options = {
    start_value: null
  };

  if (typeof options !== 'undefined') {
    this.options = options;
  }

  this.min = Helpers.min(data);
  this.max = this.options.max || Helpers.max(data);
  this.standard_deviation = Helpers.standard_deviation(data);
  this.range = 0;
  this.step = this.labelStep(this.max - this.min);
  this.start_value = this.calculateStart();
  this.process();
};

Ico.Normaliser.prototype = {
  /**
   * Calculates the start value.  This is often 0.
   * @returns {Float} The start value 
   */
  calculateStart: function() {
    var min = typeof this.options.start_value !== 'undefined' && this.min >= 0 ? this.options.start_value : this.min,
        start_value = this.round(min, 1);

    /* This is a boundary condition */
    if (this.min > 0 && start_value > this.min) {
      return 0;
    }

    if (this.min === this.max) {
      return 0;
    }

    return start_value;
  },

  /* Given a value, this method rounds it to the nearest good value for an origin */
  round: function(value, offset) {
    offset = offset || 1;
    var roundedValue = value;

    if (this.standard_deviation > 0.1) {
      var multiplier = Math.pow(10, -offset);
      roundedValue = Math.round(value * multiplier) / multiplier;

      if (roundedValue > this.min) {
        return this.round(value - this.step);
      }
    }
    return roundedValue;
  },

  /**
   * Calculates the range and step values.
   */
  process: function() {
    this.range = this.max - this.start_value;
    this.step = this.labelStep(this.range);
  },

  /**
   * Calculates the label step value.
   *
   * @param {Float} value A value to convert to a label position
   * @returns {Float} The rounded label step result
   */
  labelStep: function(value) {
    return Math.pow(10, Math.round((Math.log(value) / Math.LN10)) - 1);
  }
};


/*!
 * Ico
 * Copyright (C) 2009-2011 Alex R. Young
 * MIT Licensed
 */

/**
 * The Ico.Base object which contains useful generic functions.
 */
Ico.Base = {
  /**
   * Runs this.normalise on each value.
   *
   * @param {Array} data Values to normalise
   * @returns {Array} Normalised values
   */
  normaliseData: function(data) {
    var values = [],
        i      = 0;
    for (i = 0; i < data.length; i++) {
      values.push(this.normalise(data[i]));
    }
    return values;
  },

  /**
   * Flattens objects into an array.
   *
   * @param {Object} data Values to flatten
   * @returns {Array} Flattened values
   */
  flatten: function(data) {
    var flat_data = [];

    if (typeof data.length === 'undefined') {
      if (typeof data === 'object') {
        for (var key in data) {
          if (data.hasOwnProperty(key))
            flat_data = flat_data.concat(this.flatten(data[key]));
        }
      } else {
        return [];
      }
    }

    for (var i = 0; i < data.length; i++) {
      if (typeof data[i].length === 'number') {
        flat_data = flat_data.concat(this.flatten(data[i]));
      } else {
        flat_data.push(data[i]);
      }
    }
    return flat_data;
  },

  /**
   * Handy method to produce an array of numbers.
   *
   * @param {Integer} start A number to start at
   * @param {Integer} end A number to end at
   * @returns {Array} An array of values
   */
  makeRange: function(start, end, options) {
    var values = [], i;
    for (i = start; i < end; i++) {
      if (options && options.skip) {
        if (i % options.skip === 0) {
          values.push(i);
        } else {
          values.push(undefined);
        }
      } else {
        values.push(i);
      }
    }
    return values;
  }
};
/**
 * Ico.BaseGraph is extended by most of the other graphs.  It
 * uses a simple pattern with methods that can be overridden.
 */
Ico.BaseGraph = function() { this.initialize.apply(this, arguments); };
Helpers.extend(Ico.BaseGraph.prototype, Ico.Base);
Helpers.extend(Ico.BaseGraph.prototype, {
  /**
   * This base class is used by the other graphs in Ico.
   * 
   * Options:
   *
   *   `width`: The width of the container element
   *   `height`: The height of the container element
   *   `labels`: The textual labels
   *   `label_count`: The number of numerical labels to display
   *   `label_step`: The value to increment each numerical label
   *   `start_value`: The value to start plotting from (generally 0)
   *   `bar_size`: Set the size for a bar in a bar chart
   *   `max_bar_size`: Set the maximum size for a bar in a bar chart
   *
   * @param {Object} A DOM element
   * @param {Array|Object} Data to display
   * @param {Object} Options
   *
   */
  initialize: function(element, data, options) {
    options = options || {};

    this.element = element.length ? element[0] : element;
    this.data_sets = this.buildDataSets(data, options);
    this.flat_data = this.flatten(data);
    this.data_size = this.longestDataSetLength();
    this.plottedCoords = [];

    /* If one colour is specified, map it to a compatible set */
    if (options && options.colour) {
      if (!options.colours) options.colours = {};
      for (var key in this.data_sets) {
        if (this.data_sets.hasOwnProperty(key))
          options.colours[key] = options.colour;
      }
    }

    this.options = {
      width:                  parseInt(getStyle(element, 'width'), 10),
      height:                 parseInt(getStyle(element, 'height'), 10),
      plot_padding:           10,                                    // Padding for the graph line/bar plots
      font_size:              10,                                    // Label font size
      show_horizontal_labels: true,
      show_vertical_labels:   true,
      background_colour:      getStyle(element, 'backgroundColor') || '#ffffff',
      label_colour:           '#666',                                // Label text colour
      markers:                false,                                 // false, circle
      marker_size:            5,
      meanline:               false,
      grid:                   false,
      grid_colour:            '#ccc',
      y_padding_top:          20,
      draw:                   true,
      bar_labels:             false                                  // Display values on the top of bars
    };

    Helpers.extend(this.options, this.chartDefaults() || {});
    Helpers.extend(this.options, options);

    this.font_options = { 'font': this.options.font_size + 'px "Arial"', stroke: 'none', fill: '#000' };
    this.normaliser = new Ico.Normaliser(this.flat_data, this.normaliserOptions());
    this.label_step = options.label_step || this.normaliser.step;

    this.range = this.normaliser.range;
    this.start_value = options.start_value || this.normaliser.start_value;

    /* Padding around the graph area to make room for labels */
    this.x_padding_left = 10 + this.paddingLeftOffset();
    this.x_padding_right = 20;
    this.x_padding = this.x_padding_left + this.x_padding_right;
    this.y_padding_top = this.options.y_padding_top;

    this.y_padding_bottom = 20 + this.paddingBottomOffset();
    this.y_padding = this.y_padding_top + this.y_padding_bottom;
    
    this.graph_width = this.options.width - (this.x_padding);
    this.graph_height = this.options.height - (this.y_padding);

    this.step = this.calculateStep();
    this.grid_step = this.step;

    if (this.options.labels) {
      if (!this.grouped) {
        this.grid_step = this.graph_width / this.options.labels.length;
        this.snap_to_grid = true;
      }
    } else {
      this.options.labels = this.makeRange(1, this.data_size + 1);
    }

    if (this.options.bar_labels) {
      // TODO: Improve this so extra padding is used instead
      this.range += this.normaliser.step;
    }

    /* Calculate how many labels are required */
    if (options.label_count) {
      this.y_label_count = options.label_count;
    } else {
      if (this.range === 0) {
        this.y_label_count = 1;
        this.label_step = 1;
      } else {
        this.y_label_count = Math.ceil(this.range / this.label_step);
        if ((this.normaliser.min + (this.y_label_count * this.normaliser.step)) < this.normaliser.max) {
          this.y_label_count += 1;
        }
      }
    }

    this.value_labels = this.makeValueLabels(this.y_label_count);
    this.top_value = this.value_labels[this.value_labels.length - 1];

    /* Grid control options */
    this.grid_start_offset = -1;

    /* Drawing */
    if (this.options.draw) {
      if (typeof this.options.colours === 'undefined') {
        this.options.colours = this.makeRandomColours();
      }

      this.paper = Raphael(this.element, this.options.width, this.options.height);
      this.background = this.paper.rect(0, 0, this.options.width, this.options.height);
      this.background.attr({fill: this.options.background_colour, stroke: 'none' });

      if (this.options.meanline === true) {
        this.options.meanline = { 'stroke-width': '2px', stroke: '#BBBBBB' };
      }

      this.setChartSpecificOptions();
      this.lastPoint = { x: 0, y: 0 };
      this.draw();
    }
  },

  buildDataSets: function(data, options) {
    return (typeof data.length !== 'undefined') ? { 'one': data } : data;
  },

  normaliserOptions: function() {
    return {};
  },
  
  chartDefaults: function() {
    /* Define in child class */
    return {};
  },
 
  drawPlot: function(index, pathString, x, y, colour) {
    /* Define in child class */
  },
  
  calculateStep: function() {
    /* Define in child classes */
  },
  
  makeRandomColours: function() {
    var colours;
    if (this.grouped) {
      colours = [];
      // Colours are supplied as integers for groups, because there's no obvious way to associate the bar name
      for (var i = 0; i < this.group_size; i++) {
        colours.push(Raphael.hsb2rgb(Math.random(), 1, 0.75).hex);
      }
    } else {
      colours = {};
      for (var key in this.data_sets) {
        if (!colours.hasOwnProperty(key))
          colours[key] = Raphael.hsb2rgb(Math.random(), 1, 0.75).hex;
      }
    }
    return colours;
  },
  
  longestDataSetLength: function() {
    if (this.grouped) {
      // Return the total number of grouped values rather than
      // the longest array of data
      return this.flat_data.length;
    }

    var length = 0;
    for (var key in this.data_sets) {
      if (this.data_sets.hasOwnProperty(key)) {
        length = this.data_sets[key].length > length ? this.data_sets[key].length : length;
      }
    }
    return length;
  },
  
  roundValue: function(value, length) {
    var multiplier = Math.pow(10, length);
    value *= multiplier;
    value = Math.round(value) / multiplier;
    return value;
  },
  
  roundValues: function(data, length) {
    var roundedData = [], i;
    for (i = 0; i < data.length; i++) {
      roundedData.push(this.roundValue(data[i], length));
    }
    return roundedData;
  },

  longestLabel: function(values) {
    var labels = Array.prototype.slice.call(values || this.options.labels, 0);
    if (labels.length) {
      return labels.sort(function(a, b) { return a.toString().length < b.toString().length; })[0].toString().length;
    }
    return 0;
  },

  paddingLeftOffset: function() {
    /* Find the longest label and multiply it by the font size */
    var data = this.roundValues(this.flat_data, 2),
        longest_label_length = 0;

    longest_label_length = data.sort(function(a, b) { 
      return a.toString().length < b.toString().length;
    })[0].toString().length;

    longest_label_length = longest_label_length > 2 ? longest_label_length - 1 : longest_label_length;
    return 10 + (longest_label_length * this.options.font_size);
  },
  
  paddingBottomOffset: function() {
    /* Find the longest label and multiply it by the font size */
    return this.options.font_size;
  },
  
  normalise: function(value) {
    if (value === 0) {
      return 0;
    }

    var total = this.start_value === 0 ? this.top_value : this.range;
    return ((value / total) * (this.graph_height));
  },
  
  draw: function() {
    if (this.options.grid) {
      this.drawGrid();
    }

    if (this.options.meanline) {
      this.drawMeanLine(this.normaliseData(this.flat_data));
    }

    this.drawAxis();
    
    if (this.options.show_vertical_labels) {
      this.drawVerticalLabels();
    }
    
    if (this.options.show_horizontal_labels) {
      this.drawHorizontalLabels();
    }

    if (this.grouped) {
      this.drawLines(key, null, this.normaliseData(this.flat_data));
    } else {
      for (var key in this.data_sets) {
        if (this.data_sets.hasOwnProperty(key)) {
          var data = this.data_sets[key];
          this.drawLines(key, this.options.colours[key], this.normaliseData(data));
        }
      }
    }
    
    if (this.start_value !== 0) {
      this.drawFocusHint();
    }
  },

  drawGrid: function() {
    var pathString = '', i, y;

    if (this.options.show_vertical_labels) {
      y = this.graph_height + this.y_padding_top;
      for (i = 0; i < this.y_label_count; i++) {
        y = y - (this.graph_height / this.y_label_count);
        pathString += 'M' + this.x_padding_left + ',' + y;
        pathString += 'L' + (this.x_padding_left + this.graph_width) + ',' + y;
      }
    }

    if (this.options.show_horizontal_labels) {
      var x = this.x_padding_left + this.options.plot_padding + this.grid_start_offset,
          x_labels = this.grouped ? this.flat_data.length : this.options.labels.length,
          i,
          step = this.grid_step || this.step;

      for (i = 0; i < x_labels; i++) {
        pathString += 'M' + x + ',' + this.y_padding_top;
        pathString += 'L' + x +',' + (this.y_padding_top + this.graph_height);
        x = x + step;
      }

      x = x - this.options.plot_padding - 1;
      pathString += 'M' + x + ',' + this.y_padding_top;
      pathString += 'L' + x + ',' + (this.y_padding_top + this.graph_height);
    }

    this.paper.path(pathString).attr({ stroke: this.options.grid_colour, 'stroke-width': '1px' });
  },

  drawLines: function(label, colour, data) {
    var coords = this.calculateCoords(data),
        pathString = '',
        i, x, y;

    for (i = 0; i < coords.length; i++) {
      x = coords[i][0] || 0;
      y = coords[i][1] || 0;

      if (this.grouped && this.options.colours) {
        colour = this.options.colours[i % this.group_size];
      }

      this.plottedCoords.push([x + this.bar_padding, y]);
      pathString = this.drawPlot(i, pathString, x, y, colour);
    }

    this.paper.path(pathString).attr({ stroke: colour, 'stroke-width': this.options.stroke_width });

    if (this.options.bar_labels) {
      this.drawBarMarkers();
    }

    if (this.options.line) {
      this.additionalLine(label, colour, this.normaliseData(this.options.line));
    }
  },

  additionalLine: function(label, colour, data) {
    var coords = this.calculateCoords(data),
        pathString = '',
        i, x, y, step, lineWidth = 3;

    if (this.grouped) {
      step = this.step * (this.group_size - 1);
    }

    for (i = 0; i < coords.length; i++) {
      x = coords[i][0] || 0;
      y = coords[i][1] || 0;

      if (this.grouped) {
        x += (step * i) + this.roundValue(step / 2, 0);
      }
      x += lineWidth;

      pathString = Ico.LineGraph.prototype.drawPlot.apply(this, [i, pathString, this.roundValue(x, 0), y, colour]);
    }

    this.paper.path(pathString).attr({ stroke: '#ff0000', 'stroke-width': lineWidth + 'px' });
  },

  calculateCoords: function(data) {
    var x = this.x_padding_left + this.options.plot_padding - this.step,
        y_offset = (this.graph_height + this.y_padding_top) + this.normalise(this.start_value),
        y = 0,
        coords = [],
        i;

    for (i = 0; i < data.length; i++) {
      y = y_offset - data[i];
      x = x + this.step;
      coords.push([x, y]);
    }

    return coords;
  },

  drawFocusHint: function() {
    var length = 5,
        x = this.x_padding_left + (length / 2) - 1,
        y = this.options.height - this.y_padding_bottom,
        pathString = '';

    pathString += 'M' + x + ',' + y;
    pathString += 'L' + (x - length) + ',' + (y - length);
    pathString += 'M' + x + ',' + (y - length);
    pathString += 'L' + (x - length) + ',' + (y - (length * 2));
    this.paper.path(pathString).attr({ stroke: this.options.label_colour, 'stroke-width': 2 });
  },

  drawMeanLine: function(data) {
    var offset = Helpers.sum(data) / data.length,
        pathString = '';
 
    pathString += 'M' + (this.x_padding_left - 1) + ',' + (this.options.height - this.y_padding_bottom - offset);
    pathString += 'L' + (this.graph_width + this.x_padding_left) + ',' + (this.options.height - this.y_padding_bottom - offset);
    this.paper.path(pathString).attr(this.options.meanline);
  },

  drawAxis: function() {
    var pathString = '';

    pathString += 'M' + (this.x_padding_left - 1) + ',' + (this.options.height - this.y_padding_bottom);
    pathString += 'L' + (this.graph_width + this.x_padding_left) + ',' + (this.options.height - this.y_padding_bottom); 
    pathString += 'M' + (this.x_padding_left - 1) + ',' + (this.options.height - this.y_padding_bottom); 
    pathString += 'L' + (this.x_padding_left - 1) + ',' + (this.y_padding_top);

    this.paper.path(pathString).attr({ stroke: this.options.label_colour });
  },
  
  makeValueLabels: function(steps) {
    var step = this.label_step,
        label = this.start_value,
        labels = [];

    for (var i = 0; i < steps; i++) {
      label = this.roundValue((label + step), 2);
      labels.push(label);
    }
    return labels;
  },
 
  /* Axis label markers */
  drawMarkers: function(labels, direction, step, start_offset, font_offsets, extra_font_options) {
    function x_offset(value) {
      return value * direction[0];
    }
    
    function y_offset(value) {
      return value * direction[1];
    }
    
    /* Start at the origin */
    var x = this.x_padding_left - 1 + x_offset(start_offset),
        y = this.options.height - this.y_padding_bottom + y_offset(start_offset),
        pathString = '',
        i,
        font_options = {};
    Helpers.extend(font_options, this.font_options);
    Helpers.extend(font_options, extra_font_options || {});

    for (i = 0; i < labels.length; i++) {
      pathString += 'M' + x + ',' + y;
      if (typeof labels[i] !== 'undefined' && (labels[i] + '').length > 0) {
        pathString += 'L' + (x + y_offset(5)) + ',' + (y + x_offset(5));
        this.paper.text(x + font_offsets[0], y - font_offsets[1], labels[i]).attr(font_options).toFront();
      }
      x = x + x_offset(step);
      y = y + y_offset(step);
    }

    this.paper.path(pathString).attr({ stroke: this.options.label_colour });
  },
  
  drawVerticalLabels: function() {
    var y_step = this.graph_height / this.y_label_count;
    this.drawMarkers(this.value_labels, [0, -1], y_step, y_step, [-8, -2], { 'text-anchor': 'end' });
  },
  
  drawHorizontalLabels: function() {
    var step = this.snap_to_grid ? this.grid_step : this.step;
    this.drawMarkers(this.options.labels, [1, 0], step, this.options.plot_padding, [0, (this.options.font_size + 7) * -1]);
  }
});
/**
 * The BarGraph class.
 *
 * Example:
 * 
 *         new Ico.BarGraph($('bargraph'), [100, 10, 90, 20, 80, 30]);
 *
 */
Ico.BarGraph = function() { this.initialize.apply(this, arguments); };
Helpers.extend(Ico.BarGraph.prototype, Ico.BaseGraph.prototype);
Helpers.extend(Ico.BarGraph.prototype, {
  // Overridden to handle grouped bar graphs
  buildDataSets: function(data, options) {
    if (typeof data.length !== 'undefined') {
      if (typeof data[0].length !== 'undefined') {
        this.grouped = true;

        // TODO: Find longest?
        this.group_size = data[0].length;
        var o = {}, k, i = 0;
        for (k in options.labels) {
          k = options.labels[k];
          o[k] = data[i];
          i++;
        }
        return o;
      } else {
        return { 'one': data };
      }
    } else {
      return data;
    }
  },

  /**
   * Sensible defaults for BarGraph.
   */
  chartDefaults: function() {
    return { plot_padding: 0 };
  },

  /**
   * Ensures the normalises is always 0.
   */
  normaliserOptions: function() {
    // Make sure the true largest value is used for max
    return this.options.line ? { start_value: 0, max: Helpers.max([Helpers.max(this.options.line), Helpers.max(this.flat_data)]) } : { start_value: 0 };
  },

  /**
   * Options specific to BarGraph.
   */
  setChartSpecificOptions: function() {
    this.bar_padding = this.options.bar_padding || 5;
    this.bar_width = this.options.bar_size || this.calculateBarWidth();

    if (this.options.bar_size && !this.options.bar_padding) {
      this.bar_padding = this.graph_width / this.data_size;
    }

    this.options.plot_padding = (this.bar_width / 2) - (this.bar_padding / 2);
    this.step = this.calculateStep();
    this.grid_start_offset = this.bar_padding - 1;
    this.start_y = this.options.height - this.y_padding_bottom;
  },

  /**
   * Calculates the width of each bar.
   *
   * @returns {Integer} The bar width
   */
  calculateBarWidth: function() {
    var width = (this.graph_width / this.data_size) - this.bar_padding;

    if (this.grouped) {
      //width = width / this.group_size - (this.bar_padding * this.group_size);
    }

    if (this.options.max_bar_size && width > this.options.max_bar_size) {
      width = this.options.max_bar_size;
      this.bar_padding = this.graph_width / this.data_size;
    }

    return width;
  },

  /**
   * Calculates step used to move from one bar to another.
   * 
   * @returns {Float} The start value 
   */
  calculateStep: function() {
    return (this.graph_width - (this.options.plot_padding * 2) - (this.bar_padding * 2)) / validStepDivider(this.data_size);
  },

  /**
   * Generates paths for Raphael.
   *
   * @param {Integer} index The index of the data value to plot
   * @param {String} pathString The pathString so far
   * @param {Integer} x The x-coord to plot 
   * @param {Integer} y The y-coord to plot 
   * @param {String} colour A string that represents a colour
   * @returns {String} The resulting path string 
   */
  drawPlot: function(index, pathString, x, y, colour) {
    if (this.options.highlight_colours && this.options.highlight_colours.hasOwnProperty(index)) {
      colour = this.options.highlight_colours[index];
    }

    x = x + this.bar_padding;
    pathString += 'M' + x + ',' + this.start_y;
    pathString += 'L' + x + ',' + y;
    this.paper.path(pathString).attr({ stroke: colour, 'stroke-width': this.bar_width + 'px' });
    pathString = '';
    x = x + this.step;
    pathString += 'M' + x + ',' + this.start_y;
    return pathString;
  },

  /* Change the standard options to correctly offset against the bars */
  drawHorizontalLabels: function() {
    var x_start = this.bar_padding + this.options.plot_padding,
        step = this.step;
    if (this.grouped) {
      step = step * this.group_size;
      x_start = ((this.bar_width * this.group_size) + (this.bar_padding * this.group_size)) / 2
      x_start = this.roundValue(x_start, 0);
    }
    this.drawMarkers(this.options.labels, [1, 0], step, x_start, [0, (this.options.font_size + 7) * -1]);
  },

  drawBarMarkers: function() {
    if (this.plottedCoords.length === 0) {
      return;
    }

    var i, length = this.flat_data.length, x, y, label, font_options = {};
    Helpers.extend(font_options, this.font_options);
    font_options['text-anchor'] = 'center';

    for (i = 0; i < length; i++) {
      label = this.roundValue(this.flat_data[i], 2).toString();
      x = this.plottedCoords[i][0];
      y = this.roundValue(this.plottedCoords[i][1], 0);
      this.paper.text(x, y - this.options.font_size, label).attr(font_options).toFront();
    }
  }
});

/**
 * Draws horizontal bar graphs.
 *
 * Example:
 *
 *       new Ico.HorizontalBarGraph(element,
 *          [2, 5, 1, 10, 15, 33, 20, 25, 1],
 *          { font_size: 14 });        
 * 
 */
Ico.HorizontalBarGraph = function() { this.initialize.apply(this, arguments); };
Helpers.extend(Ico.HorizontalBarGraph.prototype, Ico.BaseGraph.prototype);
Helpers.extend(Ico.HorizontalBarGraph.prototype, {
  setChartSpecificOptions: function() {
    // Approximate the width required by the labels
    this.y_padding_top = 0;
    this.x_padding_left = 20 + this.longestLabel() * (this.options.font_size / 2);
    this.bar_padding = this.options.bar_padding || 5;
    this.bar_width = this.options.bar_size || this.calculateBarHeight();

    if (this.options.bar_size && !this.options.bar_padding) {
      this.bar_padding = this.graph_height / this.data_size;
    }

    this.options.plot_padding = 0;
    this.step = this.calculateStep();
  },

  normalise: function(value) {
    var offset = this.x_padding_left;
    return ((value / this.range) * (this.graph_width - offset));
  },

  /* Height */
  calculateBarHeight: function() {
    var height = (this.graph_height / this.data_size) - this.bar_padding;

    if (this.options.max_bar_size && height > this.options.max_bar_size) {
      height = this.options.max_bar_size;
      this.bar_padding = this.graph_height / this.data_size;
    }

    return height;
  },
  
  calculateStep: function() {
    return (this.options.height - this.y_padding_bottom) / validStepDivider(this.data_size);
  },
  
  drawLines: function(label, colour, data) {
    var x = this.x_padding_left + (this.options.plot_padding * 2),
        y = this.options.height - this.y_padding_bottom - (this.step / 2),
        pathString = 'M' + x + ',' + y,
        i;

    for (i = 0; i < data.length; i++) {
      pathString += 'L' + (x + data[i] - this.normalise(this.start_value)) + ',' + y;
      y = y - this.step;
      pathString += 'M' + x + ',' + y;
    }
    this.paper.path(pathString).attr({stroke: colour, 'stroke-width': this.bar_width + 'px'});
  },

  /* Horizontal version */
  drawFocusHint: function() {
    var length = 5,
        x = this.x_padding_left + (this.step * 2),
        y = this.options.height - this.y_padding_bottom,
        pathString = '';
    
    pathString += 'M' + x + ',' + y;
    pathString += 'L' + (x - length) + ',' + (y + length);
    pathString += 'M' + (x - length) + ',' + y;
    pathString += 'L' + (x - (length * 2)) + ',' + (y + length);
    this.paper.path(pathString).attr({stroke: this.options.label_colour, 'stroke-width': 2});
  },
  
  drawVerticalLabels: function() {
    var y_start = (this.step / 2) - (this.options.plot_padding * 2);
    this.drawMarkers(this.options.labels, [0, -1], this.step, y_start, [-8, (this.options.font_size / 8)], { 'text-anchor': 'end' });
  },
  
  drawHorizontalLabels: function() {
    var x_step = this.graph_width / this.y_label_count,
        x_labels = this.makeValueLabels(this.y_label_count);
    this.drawMarkers(x_labels, [1, 0], x_step, x_step, [0, (this.options.font_size + 7) * -1]);
  }
});

/**
 * Draws line graphs.
 *
 * Example:
 *
 *        new Ico.LineGraph(element, [10, 5, 22, 44, 4]); 
 * 
 */
Ico.LineGraph = function() { this.initialize.apply(this, arguments); };
Helpers.extend(Ico.LineGraph.prototype, Ico.BaseGraph.prototype);
Helpers.extend(Ico.LineGraph.prototype, {
  normalise: function(value) {
    if (value === 0) {
      return 0;
    }

    var total = this.start_value === 0 ? this.top_value : this.top_value - this.start_value;
    return ((value / total) * (this.graph_height));
  },

  chartDefaults: function() {
    return { plot_padding: 10, stroke_width: '3px' };
  },

  setChartSpecificOptions: function() {
    // Approximate the width required by the labels
    var longestLabel = this.longestLabel(this.value_labels);
    this.x_padding_left = 30 + longestLabel * (this.options.font_size / 2);

    if (typeof this.options.curve_amount === 'undefined') {
      this.options.curve_amount = 10;
    }
  },

  normaliserOptions: function() {
    return { start_value: this.options.start_value };
  },

  calculateStep: function() {
    return this.graph_width / this.data_size;
  },

  drawPlot: function(index, pathString, x, y, colour) {
    var w = this.options.curve_amount;

    if (this.options.markers === 'circle') {
      var circle = this.paper.circle(x, y, this.options.marker_size);
      circle.attr({ 'stroke-width': '1px', stroke: this.options.background_colour, fill: colour });
    }
    
    if (index === 0) {
      this.lastPoint = { x: x, y: y };
      return pathString + 'M' + x + ',' + y;
    }

    if (w) {
      pathString += ['C', this.lastPoint.x + w, this.lastPoint.y, x - w, y, x, y];
    } else {
      pathString += 'L' + x + ',' + y;
    }

    this.lastPoint = { x: x, y: y };
    return pathString;
  }
});

/**
 * Draws spark line graphs.
 *
 * Example:
 *
 *         new Ico.SparkLine(element,
 *           [21, 41, 32, 1, 10, 5, 32, 10, 23],
 *           { width: 30, height: 14,
 *             background_colour: '#ccc' });
 *        
 * 
 */
Ico.SparkLine = function() { this.initialize.apply(this, arguments); };
Ico.SparkLine.prototype = {
  initialize: function(element, data, options) {
    this.element = element;
    this.data = data;
    this.options = {
      width:             parseInt(getStyle(element, 'width'), 10),
      height:            parseInt(getStyle(element, 'height'), 10),
      highlight:         false,
      background_colour: getStyle(element, 'backgroundColor') || '#ffffff',
      colour:            '#036'
    };
    Helpers.extend(this.options, options || { });

    this.step = this.calculateStep();
    this.paper = Raphael(this.element, this.options.width, this.options.height);

    if (this.options.acceptable_range) {
      this.background = this.paper.rect(0, this.options.height - this.normalise(this.options.acceptable_range[1]),
                                           this.options.width,
                                           this.options.height - this.normalise(this.options.acceptable_range[0]));
    } else {
      this.background = this.paper.rect(0, 0, this.options.width, this.options.height);
    }

    this.background.attr({fill: this.options.background_colour, stroke: 'none' });
    this.draw();
  },

  calculateStep: function() {
    return this.options.width / validStepDivider(this.data.length);
  },

  normalise: function(value) {
    return (this.options.height / Helpers.max(this.data)) * value;
  },
  
  draw: function() {
    var data = this.normaliseData(this.data);
    this.drawLines('', this.options.colour, data);
    
    if (this.options.highlight) {
      this.showHighlight(data);
    }
  },
  
  drawLines: function(label, colour, data) {
    var pathString = '',
        x = 0,
        values = data.slice(1),
        i = 0;

    pathString = 'M0,' + (this.options.height - data[0]);
    for (i = 1; i < data.length; i++) {
      x = x + this.step;
      pathString += 'L' + x +',' + Ico.round(this.options.height - data[i], 2);
    }
    this.paper.path(pathString).attr({stroke: colour});
    this.lastPoint = { x: 0, y: this.options.height - data[0] };
  },
  
  showHighlight: function(data) {
    var size = 2,
        x = this.options.width - size,
        i = this.options.highlight.index || data.length - 1,
        y = data[i] + (Math.round(size / 2));

    if (typeof(this.options.highlight.index) !== 'undefined') {
      x = this.step * this.options.highlight.index;
    }

    var circle = this.paper.circle(x, this.options.height - y, size);
    circle.attr({ stroke: false, fill: this.options.highlight.colour});
  }
};
Helpers.extend(Ico.SparkLine.prototype, Ico.Base);

/**
 * Draws spark bar graphs.
 *
 * Example:
 *
 *         new Ico.SparkBar($('sparkline_2'),
 *           [1, 5, 10, 15, 20, 15, 10, 15, 30, 15, 10],
 *           { width: 30, height: 14, background_colour: '#ccc' });
 * 
 */
Ico.SparkBar = function() { this.initialize.apply(this, arguments); };
Helpers.extend(Ico.SparkBar.prototype, Ico.SparkLine.prototype);
Helpers.extend(Ico.SparkBar.prototype, {
  calculateStep: function() {
    return this.options.width / validStepDivider(this.data.length);
  },

  drawLines: function(label, colour, data) {
    var width = this.step > 2 ? this.step - 1 : this.step,
        x = width,
        pathString  = '',
        i = 0;
    for (i = 0; i < data.length; i++) {
      pathString += 'M' + x + ',' + (this.options.height - data[i]);
      pathString += 'L' + x + ',' + this.options.height;
      x = x + this.step;
    }
    this.paper.path(pathString).attr({ stroke: colour, 'stroke-width': width });
  }
});

  /**
   * Assign the Ico object as a global property.
   */
  global.Ico = Ico;

  if (typeof exports !== 'undefined') {
    module.exports = Ico;
  }
}(typeof window === 'undefined' ? this : window));

