/**
 * Bluff - beautiful graphs in JavaScript
 * ======================================
 * 
 * Get the latest version and docs at http://bluff.jcoglan.com
 * Based on Gruff by Geoffrey Grosenbach: http://github.com/topfunky/gruff
 * 
 * Copyright (C) 2008-2010 James Coglan
 * 
 * Released under the MIT license and the GPL v2.
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl-2.0.txt
 **/

Bluff = {
  // This is the version of Bluff you are using.
  VERSION: '0.3.6',
  
  array: function(list) {
    if (list.length === undefined) return [list];
    var ary = [], i = list.length;
    while (i--) ary[i] = list[i];
    return ary;
  },
  
  array_new: function(length, filler) {
    var ary = [];
    while (length--) ary.push(filler);
    return ary;
  },
  
  each: function(list, block, context) {
    for (var i = 0, n = list.length; i < n; i++) {
      block.call(context || null, list[i], i);
    }
  },
  
  index: function(list, needle) {
    for (var i = 0, n = list.length; i < n; i++) {
      if (list[i] === needle) return i;
    }
    return -1;
  },
  
  keys: function(object) {
    var ary = [], key;
    for (key in object) ary.push(key);
    return ary;
  },
  
  map: function(list, block, context) {
    var results = [];
    this.each(list, function(item) {
      results.push(block.call(context || null, item));
    });
    return results;
  },
  
  reverse_each: function(list, block, context) {
    var i = list.length;
    while (i--) block.call(context || null, list[i], i);
  },
  
  sum: function(list) {
    var sum = 0, i = list.length;
    while (i--) sum += list[i];
    return sum;
  },
  
  Mini: {}
};

Bluff.Base = new JS.Class({
  extend: {
    // Draw extra lines showing where the margins and text centers are
    DEBUG: false,
    
    // Used for navigating the array of data to plot
    DATA_LABEL_INDEX: 0,
    DATA_VALUES_INDEX: 1,
    DATA_COLOR_INDEX: 2,
    
    // Space around text elements. Mostly used for vertical spacing
    LEGEND_MARGIN: 20,
    TITLE_MARGIN: 20,
    LABEL_MARGIN: 10,
    DEFAULT_MARGIN: 20,
    
    DEFAULT_TARGET_WIDTH:  800,
    
    THOUSAND_SEPARATOR: ','
  },
  
  // Blank space above the graph
  top_margin: null,
  
  // Blank space below the graph
  bottom_margin: null,
  
  // Blank space to the right of the graph
  right_margin: null,
  
  // Blank space to the left of the graph
  left_margin: null,
  
  // Blank space below the title
  title_margin: null,
  
  // Blank space below the legend
  legend_margin: null,
  
  // A hash of names for the individual columns, where the key is the array
  // index for the column this label represents.
  //
  // Not all columns need to be named.
  //
  // Example: {0: 2005, 3: 2006, 5: 2007, 7: 2008}
  labels: null,
  
  // Used internally for spacing.
  //
  // By default, labels are centered over the point they represent.
  center_labels_over_point: null,
  
  // Used internally for horizontal graph types.
  has_left_labels: null,
  
  // A label for the bottom of the graph
  x_axis_label: null,
  
  // A label for the left side of the graph
  y_axis_label: null,
  
  // x_axis_increment: null,
  
  // Manually set increment of the horizontal marking lines
  y_axis_increment: null,
  
  // Get or set the list of colors that will be used to draw the bars or lines.
  colors: null,
  
  // The large title of the graph displayed at the top
  title: null,
  
  // Font used for titles, labels, etc.
  font: null,
  
  font_color: null,
  
  // Prevent drawing of line markers
  hide_line_markers: null,
  
  // Prevent drawing of the legend
  hide_legend: null,
  
  // Prevent drawing of the title
  hide_title: null,
  
  // Prevent drawing of line numbers
  hide_line_numbers: null,
  
  // Message shown when there is no data. Fits up to 20 characters. Defaults
  // to "No Data."
  no_data_message: null,
  
  // The font size of the large title at the top of the graph
  title_font_size: null,
  
  // Optionally set the size of the font. Based on an 800x600px graph.
  // Default is 20.
  //
  // Will be scaled down if graph is smaller than 800px wide.
  legend_font_size: null,
  
  // The font size of the labels around the graph
  marker_font_size: null,
  
  // The color of the auxiliary lines
  marker_color: null,
  
  // The number of horizontal lines shown for reference
  marker_count: null,
  
  // You can manually set a minimum value instead of having the values
  // guessed for you.
  //
  // Set it after you have given all your data to the graph object.
  minimum_value: null,
  
  // You can manually set a maximum value, such as a percentage-based graph
  // that always goes to 100.
  //
  // If you use this, you must set it after you have given all your data to
  // the graph object.
  maximum_value: null,
  
  // Set to false if you don't want the data to be sorted with largest avg
  // values at the back.
  sort: null,
  
  // Experimental
  additional_line_values: null,
  
  // Experimental
  stacked: null,
  
  // Optionally set the size of the colored box by each item in the legend.
  // Default is 20.0
  //
  // Will be scaled down if graph is smaller than 800px wide.
  legend_box_size: null,
  
  // Set to true to enable tooltip displays
  tooltips: false,
  
  // If one numerical argument is given, the graph is drawn at 4/3 ratio
  // according to the given width (800 results in 800x600, 400 gives 400x300,
  // etc.).
  //
  // Or, send a geometry string for other ratios ('800x400', '400x225').
  initialize: function(renderer, target_width) {
    this._d = new Bluff.Renderer(renderer);
    target_width = target_width || this.klass.DEFAULT_TARGET_WIDTH;
    
    var geo;
    
    if (typeof target_width !== 'number') {
      geo = target_width.split('x');
      this._columns = parseFloat(geo[0]);
      this._rows = parseFloat(geo[1]);
    } else {
      this._columns = parseFloat(target_width);
      this._rows = this._columns * 0.75;
    }
    
    this.initialize_ivars();
    
    this._reset_themes();
    this.theme_keynote();
    
    this._listeners = {};
  },
  
  // Set instance variables for this object.
  //
  // Subclasses can override this, call super, then set values separately.
  //
  // This makes it possible to set defaults in a subclass but still allow
  // developers to change this values in their program.
  initialize_ivars: function() {
    // Internal for calculations
    this._raw_columns = 800;
    this._raw_rows = 800 * (this._rows/this._columns);
    this._column_count = 0;
    this.marker_count = null;
    this.maximum_value = this.minimum_value = null;
    this._has_data = false;
    this._data = [];
    this.labels = {};
    this._labels_seen = {};
    this.sort = true;
    this.title = null;
    
    this._scale = this._columns / this._raw_columns;
    
    this.marker_font_size = 21.0;
    this.legend_font_size = 20.0;
    this.title_font_size = 36.0;
    
    this.top_margin = this.bottom_margin =
    this.left_margin = this.right_margin = this.klass.DEFAULT_MARGIN;
    
    this.legend_margin = this.klass.LEGEND_MARGIN;
    this.title_margin = this.klass.TITLE_MARGIN;
    
    this.legend_box_size = 20.0;
    
    this.no_data_message = "No Data";
    
    this.hide_line_markers = this.hide_legend = this.hide_title = this.hide_line_numbers = false;
    this.center_labels_over_point = true;
    this.has_left_labels = false;
    
    this.additional_line_values = [];
    this._additional_line_colors = [];
    this._theme_options = {};
    
    this.x_axis_label = this.y_axis_label = null;
    this.y_axis_increment = null;
    this.stacked = null;
    this._norm_data = null;
  },
  
  // Sets the top, bottom, left and right margins to +margin+.
  set_margins: function(margin) {
    this.top_margin = this.left_margin = this.right_margin = this.bottom_margin = margin;
  },
  
  // Sets the font for graph text to the font at +font_path+.
  set_font: function(font_path) {
    this.font = font_path;
    this._d.font = this.font;
  },
  
  // Add a color to the list of available colors for lines.
  //
  // Example:
  //  add_color('#c0e9d3')
  add_color: function(colorname) {
    this.colors.push(colorname);
  },
  
  // Replace the entire color list with a new array of colors. Also
  // aliased as the colors= setter method.
  //
  // If you specify fewer colors than the number of datasets you intend
  // to draw, 'increment_color' will cycle through the array, reusing
  // colors as needed.
  //
  // Note that (as with the 'set_theme' method), you should set up the color
  // list before you send your data (via the 'data' method). Calls to the
  // 'data' method made prior to this call will use whatever color scheme
  // was in place at the time data was called.
  //
  // Example:
  //  replace_colors ['#cc99cc', '#d9e043', '#34d8a2']
  replace_colors: function(color_list) {
    this.colors = color_list || [];
    this._color_index = 0;
  },
  
  // You can set a theme manually. Assign a hash to this method before you
  // send your data.
  //
  //  graph.set_theme({
  //    colors: ['orange', 'purple', 'green', 'white', 'red'],
  //    marker_color: 'blue',
  //    background_colors: ['black', 'grey']
  //  })
  //
  // background_image: 'squirrel.png' is also possible.
  //
  // (Or hopefully something better looking than that.)
  //
  set_theme: function(options) {
    this._reset_themes();
    
    this._theme_options = {
      colors: ['black', 'white'],
      additional_line_colors: [],
      marker_color: 'white',
      font_color: 'black',
      background_colors: null,
      background_image: null
    };
    for (var key in options) this._theme_options[key] = options[key];
    
    this.colors = this._theme_options.colors;
    this.marker_color = this._theme_options.marker_color;
    this.font_color = this._theme_options.font_color || this.marker_color;
    this._additional_line_colors = this._theme_options.additional_line_colors;
    
    this._render_background();
  },
  
  // Set just the background colors
  set_background: function(options) {
    if (options.colors)
      this._theme_options.background_colors = options.colors;
    if (options.image)
      this._theme_options.background_image = options.image;
    this._render_background();
  },
  
  // A color scheme similar to the popular presentation software.
  theme_keynote: function() {
    // Colors
    this._blue = '#6886B4';
    this._yellow = '#FDD84E';
    this._green = '#72AE6E';
    this._red = '#D1695E';
    this._purple = '#8A6EAF';
    this._orange = '#EFAA43';
    this._white = 'white';
    this.colors = [this._yellow, this._blue, this._green, this._red, this._purple, this._orange, this._white];
    
    this.set_theme({
      colors: this.colors,
      marker_color: 'white',
      font_color: 'white',
      background_colors: ['black', '#4a465a']
    });
  },
  
  // A color scheme plucked from the colors on the popular usability blog.
  theme_37signals: function() {
    // Colors
    this._green = '#339933';
    this._purple = '#cc99cc';
    this._blue = '#336699';
    this._yellow = '#FFF804';
    this._red = '#ff0000';
    this._orange = '#cf5910';
    this._black = 'black';
    this.colors = [this._yellow, this._blue, this._green, this._red, this._purple, this._orange, this._black];
    
    this.set_theme({
      colors: this.colors,
      marker_color: 'black',
      font_color: 'black',
      background_colors: ['#d1edf5', 'white']
    });
  },
  
  // A color scheme from the colors used on the 2005 Rails keynote
  // presentation at RubyConf.
  theme_rails_keynote: function() {
    // Colors
    this._green = '#00ff00';
    this._grey = '#333333';
    this._orange = '#ff5d00';
    this._red = '#f61100';
    this._white = 'white';
    this._light_grey = '#999999';
    this._black = 'black';
    this.colors = [this._green, this._grey, this._orange, this._red, this._white, this._light_grey, this._black];
    
    this.set_theme({
      colors: this.colors,
      marker_color: 'white',
      font_color: 'white',
      background_colors: ['#0083a3', '#0083a3']
    });
  },
  
  // A color scheme similar to that used on the popular podcast site.
  theme_odeo: function() {
    // Colors
    this._grey = '#202020';
    this._white = 'white';
    this._dark_pink = '#a21764';
    this._green = '#8ab438';
    this._light_grey = '#999999';
    this._dark_blue = '#3a5b87';
    this._black = 'black';
    this.colors = [this._grey, this._white, this._dark_blue, this._dark_pink, this._green, this._light_grey, this._black];
    
    this.set_theme({
      colors: this.colors,
      marker_color: 'white',
      font_color: 'white',
      background_colors: ['#ff47a4', '#ff1f81']
    });
  },
  
  // A pastel theme
  theme_pastel: function() {
    // Colors
    this.colors = [
                    '#a9dada', // blue
                    '#aedaa9', // green
                    '#daaea9', // peach
                    '#dadaa9', // yellow
                    '#a9a9da', // dk purple
                    '#daaeda', // purple
                    '#dadada' // grey
                  ];
    
    this.set_theme({
      colors: this.colors,
      marker_color: '#aea9a9', // Grey
      font_color: 'black',
      background_colors: 'white'
    });
  },
  
  // A greyscale theme
  theme_greyscale: function() {
    // Colors
    this.colors = [
                    '#282828', // 
                    '#383838', // 
                    '#686868', // 
                    '#989898', // 
                    '#c8c8c8', // 
                    '#e8e8e8' // 
                  ];
    
    this.set_theme({
      colors: this.colors,
      marker_color: '#aea9a9', // Grey
      font_color: 'black',
      background_colors: 'white'
    });
  },
  
  // Parameters are an array where the first element is the name of the dataset
  // and the value is an array of values to plot.
  //
  // Can be called multiple times with different datasets for a multi-valued
  // graph.
  //
  // If the color argument is nil, the next color from the default theme will
  // be used.
  //
  // NOTE: If you want to use a preset theme, you must set it before calling
  // data().
  //
  // Example:
  //   data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00')
  data: function(name, data_points, color) {
    data_points = (data_points === undefined) ? [] : data_points;
    color = color || null;
    
    data_points = Bluff.array(data_points); // make sure it's an array
    this._data.push([name, data_points, (color || this._increment_color())]);
    // Set column count if this is larger than previous counts
    this._column_count = (data_points.length > this._column_count) ? data_points.length : this._column_count;
    
    // Pre-normalize
    Bluff.each(data_points, function(data_point, index) {
      if (data_point === undefined) return;
      
      // Setup max/min so spread starts at the low end of the data points
      if (this.maximum_value === null && this.minimum_value === null)
        this.maximum_value = this.minimum_value = data_point;
      
      // TO_IGNORE_DO Doesn't work with stacked bar graphs
      // Original: @maximum_value = _larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
      this.maximum_value = this._larger_than_max(data_point) ? data_point : this.maximum_value;
      if (this.maximum_value >= 0) this._has_data = true;
      
      this.minimum_value = this._less_than_min(data_point) ? data_point : this.minimum_value;
      if (this.minimum_value < 0) this._has_data = true;
    }, this);
  },
  
  // Overridden by subclasses to do the actual plotting of the graph.
  //
  // Subclasses should start by calling super() for this method.
  draw: function() {
    if (this.stacked) this._make_stacked();
    this._setup_drawing();
    
    this._debug(function() {
      // Outer margin
      this._d.rectangle(this.left_margin, this.top_margin,
                        this._raw_columns - this.right_margin, this._raw_rows - this.bottom_margin);
      // Graph area box
      this._d.rectangle(this._graph_left, this._graph_top, this._graph_right, this._graph_bottom);
    });
  },
  
  clear: function() {
    this._render_background();
  },
  
  on: function(eventType, callback, context) {
    var list = this._listeners[eventType] = this._listeners[eventType] || [];
    list.push([callback, context]);
  },
  
  trigger: function(eventType, data) {
    var list = this._listeners[eventType];
    if (!list) return;
    Bluff.each(list, function(listener) {
      listener[0].call(listener[1], data);
    });
  },
  
  // Calculates size of drawable area and draws the decorations.
  //
  // * line markers
  // * legend
  // * title
  _setup_drawing: function() {
    // Maybe should be done in one of the following functions for more granularity.
    if (!this._has_data) return this._draw_no_data();
    
    this._normalize();
    this._setup_graph_measurements();
    if (this.sort) this._sort_norm_data();
    
    this._draw_legend();
    this._draw_line_markers();
    this._draw_axis_labels();
    this._draw_title();
  },
  
  // Make copy of data with values scaled between 0-100
  _normalize: function(force) {
    if (this._norm_data === null || force === true) {
      this._norm_data = [];
      if (!this._has_data) return;
      
      this._calculate_spread();
      
      Bluff.each(this._data, function(data_row) {
        var norm_data_points = [];
        Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point) {
          if (data_point === null || data_point === undefined)
            norm_data_points.push(null);
          else
            norm_data_points.push((data_point - this.minimum_value) / this._spread);
        }, this);
        this._norm_data.push([data_row[this.klass.DATA_LABEL_INDEX], norm_data_points, data_row[this.klass.DATA_COLOR_INDEX]]);
      }, this);
    }
  },
  
  _calculate_spread: function() {
    this._spread = this.maximum_value - this.minimum_value;
    this._spread = this._spread > 0 ? this._spread : 1;
    
    var power = Math.round(Math.LOG10E*Math.log(this._spread));
    this._significant_digits = Math.pow(10, 3 - power);
  },
  
  // Calculates size of drawable area, general font dimensions, etc.
  _setup_graph_measurements: function() {
    this._marker_caps_height = this.hide_line_markers ? 0 :
      this._calculate_caps_height(this.marker_font_size);
    this._title_caps_height = this.hide_title ? 0 :
      this._calculate_caps_height(this.title_font_size);
    this._legend_caps_height = this.hide_legend ? 0 :
      this._calculate_caps_height(this.legend_font_size);
    
    var longest_label,
        longest_left_label_width,
        line_number_width,
        last_label,
        extra_room_for_long_label,
        x_axis_label_height,
        key;
    
    if (this.hide_line_markers) {
      this._graph_left = this.left_margin;
      this._graph_right_margin = this.right_margin;
      this._graph_bottom_margin = this.bottom_margin;
    } else {
      longest_left_label_width = 0;
      if (this.has_left_labels) {
        longest_label = '';
        for (key in this.labels) {
          longest_label = longest_label.length > this.labels[key].length
              ? longest_label
              : this.labels[key];
        }
        longest_left_label_width = this._calculate_width(this.marker_font_size, longest_label) * 1.25;
      } else {
        longest_left_label_width = this._calculate_width(this.marker_font_size, this._label(this.maximum_value));
      }
      
      // Shift graph if left line numbers are hidden
      line_number_width = this.hide_line_numbers && !this.has_left_labels ?
      0.0 :
        longest_left_label_width + this.klass.LABEL_MARGIN * 2;
      
      this._graph_left = this.left_margin +
        line_number_width +
        (this.y_axis_label === null ? 0.0 : this._marker_caps_height + this.klass.LABEL_MARGIN * 2);
      
      // Make space for half the width of the rightmost column label.
      // Might be greater than the number of columns if between-style bar markers are used.
      last_label = -Infinity;
      for (key in this.labels)
        last_label = last_label > Number(key) ? last_label : Number(key);
      last_label = Math.round(last_label);
      extra_room_for_long_label = (last_label >= (this._column_count-1) && this.center_labels_over_point) ?
      this._calculate_width(this.marker_font_size, this.labels[last_label]) / 2 :
        0;
      this._graph_right_margin  = this.right_margin + extra_room_for_long_label;
      
      this._graph_bottom_margin = this.bottom_margin +
        this._marker_caps_height + this.klass.LABEL_MARGIN;
    }
    
    this._graph_right = this._raw_columns - this._graph_right_margin;
    this._graph_width = this._raw_columns - this._graph_left - this._graph_right_margin;
    
    // When hide_title, leave a title_margin space for aesthetics.
    // Same with hide_legend
    this._graph_top = this.top_margin +
      (this.hide_title  ? this.title_margin  : this._title_caps_height  + this.title_margin ) +
      (this.hide_legend ? this.legend_margin : this._legend_caps_height + this.legend_margin);
    
    x_axis_label_height = (this.x_axis_label === null) ? 0.0 :
      this._marker_caps_height + this.klass.LABEL_MARGIN;
    this._graph_bottom = this._raw_rows - this._graph_bottom_margin - x_axis_label_height;
    this._graph_height = this._graph_bottom - this._graph_top;
  },
  
  // Draw the optional labels for the x axis and y axis.
  _draw_axis_labels: function() {
    if (this.x_axis_label) {
      // X Axis
      // Centered vertically and horizontally by setting the
      // height to 1.0 and the width to the width of the graph.
      var x_axis_label_y_coordinate = this._graph_bottom + this.klass.LABEL_MARGIN * 2 + this._marker_caps_height;
      
      // TO_IGNORE_DO Center between graph area
      this._d.fill = this.font_color;
      if (this.font) this._d.font = this.font;
      this._d.stroke = 'transparent';
      this._d.pointsize = this._scale_fontsize(this.marker_font_size);
      this._d.gravity = 'north';
      this._d.annotate_scaled(
                              this._raw_columns, 1.0,
                              0.0, x_axis_label_y_coordinate,
                              this.x_axis_label, this._scale);
      this._debug(function() {
        this._d.line(0.0, x_axis_label_y_coordinate, this._raw_columns, x_axis_label_y_coordinate);
      });
    }
    
    // TO_IGNORE_DO Y label (not generally possible in browsers)
  },
  
  // Draws horizontal background lines and labels
  _draw_line_markers: function() {
    if (this.hide_line_markers) return;
    
    if (this.y_axis_increment === null) {
      // Try to use a number of horizontal lines that will come out even.
      //
      // TO_IGNORE_DO Do the same for larger numbers...100, 75, 50, 25
      if (this.marker_count === null) {
        Bluff.each([3,4,5,6,7], function(lines) {
          if (!this.marker_count && this._spread % lines === 0)
            this.marker_count = lines;
        }, this);
        this.marker_count = this.marker_count || 4;
      }
      this._increment = (this._spread > 0) ? this._significant(this._spread / this.marker_count) : 1;
    } else {
      // TO_IGNORE_DO Make this work for negative values
      this.maximum_value = Math.max(Math.ceil(this.maximum_value), this.y_axis_increment);
      this.minimum_value = Math.floor(this.minimum_value);
      this._calculate_spread();
      this._normalize(true);
      
      this.marker_count = Math.round(this._spread / this.y_axis_increment);
      this._increment = this.y_axis_increment;
    }
    this._increment_scaled = this._graph_height / (this._spread / this._increment);
    
    // Draw horizontal line markers and annotate with numbers
    var index, n, y, marker_label;
    for (index = 0, n = this.marker_count; index <= n; index++) {
      y = this._graph_top + this._graph_height - index * this._increment_scaled;
      
      this._d.stroke = this.marker_color;
      this._d.stroke_width = 1;
      this._d.line(this._graph_left, y, this._graph_right, y);
      
      marker_label = index * this._increment + this.minimum_value;
      
      if (!this.hide_line_numbers) {
        this._d.fill = this.font_color;
        if (this.font) this._d.font = this.font;
        this._d.font_weight = 'normal';
        this._d.stroke = 'transparent';
        this._d.pointsize = this._scale_fontsize(this.marker_font_size);
        this._d.gravity = 'east';
        
        // Vertically center with 1.0 for the height
        this._d.annotate_scaled(this._graph_left - this.klass.LABEL_MARGIN,
                                1.0, 0.0, y,
                                this._label(marker_label), this._scale);
      }
    }
  },
  
  _center: function(size) {
    return (this._raw_columns - size) / 2;
  },
  
  // Draws a legend with the names of the datasets matched to the colors used
  // to draw them.
  _draw_legend: function() {
    if (this.hide_legend) return;
    
    this._legend_labels = Bluff.map(this._data, function(item) {
      return item[this.klass.DATA_LABEL_INDEX];
    }, this);
    
    var legend_square_width = this.legend_box_size; // small square with color of this item
    
    // May fix legend drawing problem at small sizes
    if (this.font) this._d.font = this.font;
    this._d.pointsize = this.legend_font_size;
    
    var label_widths = [[]]; // Used to calculate line wrap
    Bluff.each(this._legend_labels, function(label) {
      var last = label_widths.length - 1;
      var metrics = this._d.get_type_metrics(label);
      var label_width = metrics.width + legend_square_width * 2.7;
      label_widths[last].push(label_width);
      
      if (Bluff.sum(label_widths[last]) > (this._raw_columns * 0.9))
        label_widths.push([label_widths[last].pop()]);
    }, this);
    
    var current_x_offset = this._center(Bluff.sum(label_widths[0]));
    var current_y_offset = this.hide_title ?
    this.top_margin + this.title_margin :
      this.top_margin + this.title_margin + this._title_caps_height;
    
    this._debug(function() {
      this._d.stroke_width = 1;
      this._d.line(0, current_y_offset, this._raw_columns, current_y_offset);
    });
    
    Bluff.each(this._legend_labels, function(legend_label, index) {
      
      // Draw label
      this._d.fill = this.font_color;
      if (this.font) this._d.font = this.font;
      this._d.pointsize = this._scale_fontsize(this.legend_font_size);
      this._d.stroke = 'transparent';
      this._d.font_weight = 'normal';
      this._d.gravity = 'west';
      this._d.annotate_scaled(this._raw_columns, 1.0,
                              current_x_offset + (legend_square_width * 1.7), current_y_offset,
                              legend_label, this._scale);
      
      // Now draw box with color of this dataset
      this._d.stroke = 'transparent';
      this._d.fill = this._data[index][this.klass.DATA_COLOR_INDEX];
      this._d.rectangle(current_x_offset,
                        current_y_offset - legend_square_width / 2.0,
                        current_x_offset + legend_square_width,
                        current_y_offset + legend_square_width / 2.0);
      
      this._d.pointsize = this.legend_font_size;
      var metrics = this._d.get_type_metrics(legend_label);
      var current_string_offset = metrics.width + (legend_square_width * 2.7),
          line_height;
      
      // Handle wrapping
      label_widths[0].shift();
      if (label_widths[0].length == 0) {
        this._debug(function() {
          this._d.line(0.0, current_y_offset, this._raw_columns, current_y_offset);
        });
        
        label_widths.shift();
        if (label_widths.length > 0) current_x_offset = this._center(Bluff.sum(label_widths[0]));
        line_height = Math.max(this._legend_caps_height, legend_square_width) + this.legend_margin;
        if (label_widths.length > 0) {
          // Wrap to next line and shrink available graph dimensions
          current_y_offset += line_height;
          this._graph_top += line_height;
          this._graph_height = this._graph_bottom - this._graph_top;
        }
      } else {
        current_x_offset += current_string_offset;
      }
    }, this);
    this._color_index = 0;
  },
  
  // Draws a title on the graph.
  _draw_title: function() {
    if (this.hide_title || !this.title) return;
    
    this._d.fill = this.font_color;
    if (this.font) this._d.font = this.font;
    this._d.pointsize = this._scale_fontsize(this.title_font_size);
    this._d.font_weight = 'bold';
    this._d.gravity = 'north';
    this._d.annotate_scaled(this._raw_columns, 1.0,
                            0, this.top_margin,
                            this.title, this._scale);
  },
  
  // Draws column labels below graph, centered over x_offset
  //--
  // TO_IGNORE_DO Allow WestGravity as an option
  _draw_label: function(x_offset, index) {
    if (this.hide_line_markers) return;
    
    var y_offset;
    
    if (this.labels[index] && !this._labels_seen[index]) {
      y_offset = this._graph_bottom + this.klass.LABEL_MARGIN;
      
      this._d.fill = this.font_color;
      if (this.font) this._d.font = this.font;
      this._d.stroke = 'transparent';
      this._d.font_weight = 'normal';
      this._d.pointsize = this._scale_fontsize(this.marker_font_size);
      this._d.gravity = 'north';
      this._d.annotate_scaled(1.0, 1.0,
                              x_offset, y_offset,
                              this.labels[index], this._scale);
      this._labels_seen[index] = true;
      
      this._debug(function() {
        this._d.stroke_width = 1;
        this._d.line(0.0, y_offset, this._raw_columns, y_offset);
      });
    }
  },
  
  // Creates a mouse hover target rectangle for tooltip displays
  _draw_tooltip: function(left, top, width, height, name, color, data, index) {
    if (!this.tooltips) return;
    var node = this._d.tooltip(left, top, width, height, name, color, data);
    
    Bluff.Event.observe(node, 'click', function() {
      var point = {
        series: name,
        label:  this.labels[index],
        value:  data,
        color:  color
      };
      this.trigger('click:datapoint', point);
    }, this);
  },
  
  // Shows an error message because you have no data.
  _draw_no_data: function() {
    this._d.fill = this.font_color;
    if (this.font) this._d.font = this.font;
    this._d.stroke = 'transparent';
    this._d.font_weight = 'normal';
    this._d.pointsize = this._scale_fontsize(80);
    this._d.gravity = 'center';
    this._d.annotate_scaled(this._raw_columns, this._raw_rows/2,
                            0, 10,
                            this.no_data_message, this._scale);
  },
  
  // Finds the best background to render based on the provided theme options.
  _render_background: function() {
    var colors = this._theme_options.background_colors;
    switch (true) {
      case colors instanceof Array:
        this._render_gradiated_background.apply(this, colors);
        break;
      case typeof colors === 'string':
        this._render_solid_background(colors);
        break;
      default:
        this._render_image_background(this._theme_options.background_image);
        break;
    }
  },
  
  // Make a new image at the current size with a solid +color+.
  _render_solid_background: function(color) {
    this._d.render_solid_background(this._columns, this._rows, color);
  },
  
  // Use with a theme definition method to draw a gradiated background.
  _render_gradiated_background: function(top_color, bottom_color) {
    this._d.render_gradiated_background(this._columns, this._rows, top_color, bottom_color);
  },
  
  // Use with a theme to use an image (800x600 original) background.
  _render_image_background: function(image_path) {
    // TO_IGNORE_DO
  },
  
  // Resets everything to defaults (except data).
  _reset_themes: function() {
    this._color_index = 0;
    this._labels_seen = {};
    this._theme_options = {};
    this._d.scale(this._scale, this._scale);
  },
  
  _scale_value: function(value) {
    return this._scale * value;
  },
  
  // Return a comparable fontsize for the current graph.
  _scale_fontsize: function(value) {
    var new_fontsize = value * this._scale;
    return new_fontsize;
  },
  
  _clip_value_if_greater_than: function(value, max_value) {
    return (value > max_value) ? max_value : value;
  },
  
  // Overridden by subclasses such as stacked bar.
  _larger_than_max: function(data_point, index) {
    return data_point > this.maximum_value;
  },
  
  _less_than_min: function(data_point, index) {
    return data_point < this.minimum_value;
  },
  
  // Overridden by subclasses that need it.
  _max: function(data_point, index) {
    return data_point;
  },
  
  // Overridden by subclasses that need it.
  _min: function(data_point, index) {
    return data_point;
  },
  
  _significant: function(inc) {
    if (inc == 0) return 1.0;
    var factor = 1.0;
    while (inc < 10) {
      inc *= 10;
      factor /= 10;
    }
    
    while (inc > 100) {
      inc /= 10;
      factor *= 10;
    }
    
    return Math.floor(inc) * factor;
  },
  
  // Sort with largest overall summed value at front of array so it shows up
  // correctly in the drawn graph.
  _sort_norm_data: function() {
    var sums = this._sums, index = this.klass.DATA_VALUES_INDEX;
    
    this._norm_data.sort(function(a,b) {
      return sums(b[index]) - sums(a[index]);
    });
    
    this._data.sort(function(a,b) {
      return sums(b[index]) - sums(a[index]);
    });
  },
  
  _sums: function(data_set) {
    var total_sum = 0;
    Bluff.each(data_set, function(num) { total_sum += (num || 0) });
    return total_sum;
  },
  
  _make_stacked: function() {
    var stacked_values = [], i = this._column_count;
    while (i--) stacked_values[i] = 0;
    Bluff.each(this._data, function(value_set) {
      Bluff.each(value_set[this.klass.DATA_VALUES_INDEX], function(value, index) {
        stacked_values[index] += value;
      }, this);
      value_set[this.klass.DATA_VALUES_INDEX] = Bluff.array(stacked_values);
    }, this);
  },
  
  // Takes a block and draws it if DEBUG is true.
  //
  // Example:
  //   debug { @d.rectangle x1, y1, x2, y2 }
  _debug: function(block) {
    if (this.klass.DEBUG) {
      this._d.fill = 'transparent';
      this._d.stroke = 'turquoise';
      block.call(this);
    }
  },
  
  // Returns the next color in your color list.
  _increment_color: function() {
    var offset = this._color_index;
    this._color_index = (this._color_index + 1) % this.colors.length;
    return this.colors[offset];
  },
  
  // Return a formatted string representing a number value that should be
  // printed as a label.
  _label: function(value) {
    var sep   = this.klass.THOUSAND_SEPARATOR,
        label = (this._spread % this.marker_count == 0 || this.y_axis_increment !== null)
        ? String(Math.round(value))
        : String(Math.floor(value * this._significant_digits)/this._significant_digits);
    
    var parts = label.split('.');
    parts[0] = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + sep);
    return parts.join('.');
  },
  
  // Returns the height of the capital letter 'X' for the current font and
  // size.
  //
  // Not scaled since it deals with dimensions that the regular scaling will
  // handle.
  _calculate_caps_height: function(font_size) {
    return this._d.caps_height(font_size);
  },
  
  // Returns the width of a string at this pointsize.
  //
  // Not scaled since it deals with dimensions that the regular 
  // scaling will handle.
  _calculate_width: function(font_size, text) {
    return this._d.text_width(font_size, text);
  }
});


Bluff.Area = new JS.Class(Bluff.Base, {
  
  draw: function() {
    this.callSuper();
    
    if (!this._has_data) return;
    
    this._x_increment = this._graph_width / (this._column_count - 1);
    this._d.stroke = 'transparent';
    
    Bluff.each(this._norm_data, function(data_row) {
      var poly_points = [],
          prev_x = 0.0,
          prev_y = 0.0;
      
      Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
        // Use incremented x and scaled y
        var new_x = this._graph_left + (this._x_increment * index);
        var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height);
        
        if (prev_x > 0 && prev_y > 0) {
          poly_points.push(new_x);
          poly_points.push(new_y);
          
          // this._d.polyline(prev_x, prev_y, new_x, new_y);
        } else {
          poly_points.push(this._graph_left);
          poly_points.push(this._graph_bottom - 1);
          poly_points.push(new_x);
          poly_points.push(new_y);
          
          // this._d.polyline(this._graph_left, this._graph_bottom, new_x, new_y);
        }
        
        this._draw_label(new_x, index);
        
        prev_x = new_x;
        prev_y = new_y;
      }, this);
      
      // Add closing points, draw polygon
      poly_points.push(this._graph_right);
      poly_points.push(this._graph_bottom - 1);
      poly_points.push(this._graph_left);
      poly_points.push(this._graph_bottom - 1);
      
      this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
      this._d.polyline(poly_points);
      
    }, this);
  }
});


//  This class perfoms the y coordinats conversion for the bar class.
//
//  There are three cases: 
//
//    1. Bars all go from zero in positive direction
//    2. Bars all go from zero to negative direction  
//    3. Bars either go from zero to positive or from zero to negative
//
Bluff.BarConversion = new JS.Class({
  mode:           null,
  zero:           null,
  graph_top:      null,
  graph_height:   null,
  minimum_value:  null,
  spread:         null,
  
  getLeftYRightYscaled: function(data_point, result) {
    var val;
    switch (this.mode) {
      case 1: // Case one
        // minimum value >= 0 ( only positiv values )
        result[0] = this.graph_top + this.graph_height*(1 - data_point) + 1;
        result[1] = this.graph_top + this.graph_height - 1;
        break;
      case 2:  // Case two
        // only negativ values
         result[0] = this.graph_top + 1;
        result[1] = this.graph_top + this.graph_height*(1 - data_point) - 1;
        break;
      case 3: // Case three
        // positiv and negativ values
        val = data_point-this.minimum_value/this.spread;
        if ( data_point >= this.zero ) {
          result[0] = this.graph_top + this.graph_height*(1 - (val-this.zero)) + 1;
          result[1] = this.graph_top + this.graph_height*(1 - this.zero) - 1;
        } else {
          result[0] = this.graph_top + this.graph_height*(1 - (val-this.zero)) + 1;
          result[1] = this.graph_top + this.graph_height*(1 - this.zero) - 1;
        }
        break;
      default:
        result[0] = 0.0;
        result[1] = 0.0;
    }        
  }  
  
});


Bluff.Bar = new JS.Class(Bluff.Base, {
  
  // Spacing factor applied between bars
  bar_spacing: 0.9,
  
  draw: function() {
    // Labels will be centered over the left of the bar if
    // there are more labels than columns. This is basically the same 
    // as where it would be for a line graph.
    this.center_labels_over_point = (Bluff.keys(this.labels).length > this._column_count);
    
    this.callSuper();
    if (!this._has_data) return;
    
    this._draw_bars();
  },
  
  _draw_bars: function() {
    this._bar_width = this._graph_width / (this._column_count * this._data.length);
    var padding = (this._bar_width * (1 - this.bar_spacing)) / 2;
    
    this._d.stroke_opacity = 0.0;
    
    // Setup the BarConversion Object
    var conversion = new Bluff.BarConversion();
    conversion.graph_height = this._graph_height;
    conversion.graph_top = this._graph_top;
    
    // Set up the right mode [1,2,3] see BarConversion for further explanation
    if (this.minimum_value >= 0) {
      // all bars go from zero to positiv
      conversion.mode = 1;
    } else {
      // all bars go from 0 to negativ
      if (this.maximum_value <= 0) {
        conversion.mode = 2;
      } else {
        // bars either go from zero to negativ or to positiv
        conversion.mode = 3;
        conversion.spread = this._spread;
        conversion.minimum_value = this.minimum_value;
        conversion.zero = -this.minimum_value/this._spread;
      }
    }
    
    // iterate over all normalised data
    Bluff.each(this._norm_data, function(data_row, row_index) {
      var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
      
      Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
        // Use incremented x and scaled y
        // x
        var left_x = this._graph_left + (this._bar_width * (row_index + point_index + ((this._data.length - 1) * point_index))) + padding;
        var right_x = left_x + this._bar_width * this.bar_spacing;
        // y
        var conv = [];
        conversion.getLeftYRightYscaled(data_point, conv);
        
        // create new bar
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.rectangle(left_x, conv[0], right_x, conv[1]);
        
        // create tooltip target
        this._draw_tooltip(left_x, conv[0],
                           right_x - left_x, conv[1] - conv[0],
                           data_row[this.klass.DATA_LABEL_INDEX],
                           data_row[this.klass.DATA_COLOR_INDEX],
                           raw_data[point_index], point_index);
        
        // Calculate center based on bar_width and current row
        var label_center = this._graph_left + 
                          (this._data.length * this._bar_width * point_index) + 
                          (this._data.length * this._bar_width / 2.0);
        // Subtract half a bar width to center left if requested
        this._draw_label(label_center - (this.center_labels_over_point ? this._bar_width / 2.0 : 0.0), point_index);
      }, this);
      
    }, this);
    
    // Draw the last label if requested
    if (this.center_labels_over_point) this._draw_label(this._graph_right, this._column_count);
  }
});


// Here's how to make a Line graph:
//
//   g = new Bluff.Line('canvasId');
//   g.title = "A Line Graph";
//   g.data('Fries', [20, 23, 19, 8]);
//   g.data('Hamburgers', [50, 19, 99, 29]);
//   g.draw();
//
// There are also other options described below, such as #baseline_value, #baseline_color, #hide_dots, and #hide_lines.

Bluff.Line = new JS.Class(Bluff.Base, {
  // Draw a dashed line at the given value
  baseline_value: null,
  
  // Color of the baseline
  baseline_color: null,
  
  // Dimensions of lines and dots; calculated based on dataset size if left unspecified
  line_width: null,
  dot_radius: null,
  
  // Hide parts of the graph to fit more datapoints, or for a different appearance.
  hide_dots: null,
  hide_lines: null,
  
  // Call with target pixel width of graph (800, 400, 300), and/or 'false' to omit lines (points only).
  //
  //  g = new Bluff.Line('canvasId', 400) // 400px wide with lines
  //
  //  g = new Bluff.Line('canvasId', 400, false) // 400px wide, no lines (for backwards compatibility)
  //
  //  g = new Bluff.Line('canvasId', false) // Defaults to 800px wide, no lines (for backwards compatibility)
  // 
  // The preferred way is to call hide_dots or hide_lines instead.
  initialize: function(renderer) {
    if (arguments.length > 3) throw 'Wrong number of arguments';
    if (arguments.length === 1 || (typeof arguments[1] !== 'number' && typeof arguments[1] !== 'string'))
      this.callSuper(renderer, null);
    else
      this.callSuper();
    
    this.hide_dots = this.hide_lines = false;
    this.baseline_color = 'red';
    this.baseline_value = null;
  },
  
  draw: function() {
    this.callSuper();
    
    if (!this._has_data) return;
    
    // Check to see if more than one datapoint was given. NaN can result otherwise.
    this.x_increment = (this._column_count > 1) ? (this._graph_width / (this._column_count - 1)) : this._graph_width;
    
    var level;
    
    if (this._norm_baseline !== undefined) {
      level = this._graph_top + (this._graph_height - this._norm_baseline * this._graph_height);
      this._d.push();
      this._d.stroke = this.baseline_color;
      this._d.fill_opacity = 0.0;
      // this._d.stroke_dasharray(10, 20);
      this._d.stroke_width = 3.0;
      this._d.line(this._graph_left, level, this._graph_left + this._graph_width, level);
      this._d.pop();
    }
    
    Bluff.each(this._norm_data, function(data_row, row_index) {
      var prev_x = null, prev_y = null;
      var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
      
      this._one_point = this._contains_one_point_only(data_row);
      
      Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
        var new_x = this._graph_left + (this.x_increment * index);
        if (typeof data_point !== 'number') return;
        
        this._draw_label(new_x, index);
        
        var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height);
        
        // Reset each time to avoid thin-line errors
        this._d.stroke = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.stroke_opacity = 1.0;
        this._d.stroke_width = this.line_width ||
          this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 6), 3.0);
        
        var circle_radius = this.dot_radius ||
          this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 2), 7.0);
        
        if (!this.hide_lines && prev_x !== null && prev_y !== null) {
          this._d.line(prev_x, prev_y, new_x, new_y);
        } else if (this._one_point) {
          // Show a circle if there's just one point
          this._d.circle(new_x, new_y, new_x - circle_radius, new_y);
        }
        
        if (!this.hide_dots) this._d.circle(new_x, new_y, new_x - circle_radius, new_y);
        
        this._draw_tooltip(new_x - circle_radius, new_y - circle_radius,
                           2 * circle_radius, 2 *circle_radius,
                           data_row[this.klass.DATA_LABEL_INDEX],
                           data_row[this.klass.DATA_COLOR_INDEX],
                           raw_data[index], index);
        
        prev_x = new_x;
        prev_y = new_y;
      }, this);
    }, this);
  },
  
  _normalize: function() {
    this.maximum_value = Math.max(this.maximum_value, this.baseline_value);
    this.callSuper();
    if (this.baseline_value !== null) this._norm_baseline = this.baseline_value / this.maximum_value;
  },
  
  _contains_one_point_only: function(data_row) {
    // Spin through data to determine if there is just one value present.
    var count = 0;
    Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point) {
      if (data_point !== undefined) count += 1;
    });
    return count === 1;
  }
});


// Graph with dots and labels along a vertical access
// see: 'Creating More Effective Graphs' by Robbins

Bluff.Dot = new JS.Class(Bluff.Base, {
  
  draw: function() {
    this.has_left_labels = true;
    this.callSuper();
    
    if (!this._has_data) return;
    
    // Setup spacing.
    //
    var spacing_factor = 1.0;
    
    this._items_width = this._graph_height / this._column_count;
    this._item_width = this._items_width * spacing_factor / this._norm_data.length;
    this._d.stroke_opacity = 0.0;
    var height = Bluff.array_new(this._column_count, 0),
        length = Bluff.array_new(this._column_count, this._graph_left),
        padding = (this._items_width * (1 - spacing_factor)) / 2;
    
    Bluff.each(this._norm_data, function(data_row, row_index) {
      Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
        
        var x_pos = this._graph_left + (data_point * this._graph_width) - Math.round(this._item_width/6.0);
        var y_pos = this._graph_top + (this._items_width * point_index) + padding + Math.round(this._item_width/2.0);
        
        if (row_index === 0) {
          this._d.stroke = this.marker_color;
          this._d.stroke_width = 1.0;
          this._d.opacity = 0.1;
          this._d.line(this._graph_left, y_pos, this._graph_left + this._graph_width, y_pos);
        }
        
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.stroke = 'transparent';
        this._d.circle(x_pos, y_pos, x_pos + Math.round(this._item_width/3.0), y_pos);
        
        // Calculate center based on item_width and current row
        var label_center = this._graph_top + (this._items_width * point_index + this._items_width / 2) + padding;
        this._draw_label(label_center, point_index);
      }, this);
      
    }, this);
  },
  
  // Instead of base class version, draws vertical background lines and label
  _draw_line_markers: function() {
    
    if (this.hide_line_markers) return;
    
    this._d.stroke_antialias = false;
    
    // Draw horizontal line markers and annotate with numbers
    this._d.stroke_width = 1;
    var number_of_lines = 5;
    
    // TO_IGNORE_DO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
    var increment = this._significant(this.maximum_value / number_of_lines);
    for (var index = 0; index <= number_of_lines; index++) {
      
      var line_diff    = (this._graph_right - this._graph_left) / number_of_lines,
          x            = this._graph_right - (line_diff * index) - 1,
          diff         = index - number_of_lines,
          marker_label = Math.abs(diff) * increment;
      
      this._d.stroke = this.marker_color;
      this._d.line(x, this._graph_bottom, x, this._graph_bottom + 0.5 * this.klass.LABEL_MARGIN);
      
      if (!this.hide_line_numbers) {
        this._d.fill      = this.font_color;
        if (this.font) this._d.font = this.font;
        this._d.stroke    = 'transparent';
        this._d.pointsize = this._scale_fontsize(this.marker_font_size);
        this._d.gravity   = 'center';
        // TO_IGNORE_DO Center text over line
        this._d.annotate_scaled(0, 0, // Width of box to draw text in
                                x, this._graph_bottom + (this.klass.LABEL_MARGIN * 2.0), // Coordinates of text
                                marker_label, this._scale);
      }
      this._d.stroke_antialias = true;
    }
  },
  
  // Draw on the Y axis instead of the X
  _draw_label: function(y_offset, index) {
    if (this.labels[index] && !this._labels_seen[index]) {
      this._d.fill             = this.font_color;
      if (this.font) this._d.font = this.font;
      this._d.stroke           = 'transparent';
      this._d.font_weight      = 'normal';
      this._d.pointsize        = this._scale_fontsize(this.marker_font_size);
      this._d.gravity          = 'east';
      this._d.annotate_scaled(1, 1,
                              this._graph_left - this.klass.LABEL_MARGIN * 2.0, y_offset,
                              this.labels[index], this._scale);
      this._labels_seen[index] = true;
    }
  }
});


// Experimental!!! See also the Spider graph.
Bluff.Net = new JS.Class(Bluff.Base, {
  
  // Hide parts of the graph to fit more datapoints, or for a different appearance.
  hide_dots: null,
  
  //Dimensions of lines and dots; calculated based on dataset size if left unspecified
  line_width: null,
  dot_radius: null,
  
  initialize: function() {
    this.callSuper();
    
    this.hide_dots = false;
    this.hide_line_numbers = true;
  },
  
  draw: function() {
    
    this.callSuper();
    
    if (!this._has_data) return;
    
    this._radius = this._graph_height / 2.0;
    this._center_x = this._graph_left + (this._graph_width / 2.0);
    this._center_y = this._graph_top + (this._graph_height / 2.0) - 10; // Move graph up a bit
    
    this._x_increment = this._graph_width / (this._column_count - 1);
    var circle_radius = this.dot_radius ||
      this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 2.5), 7.0);
    
    this._d.stroke_opacity = 1.0;
    this._d.stroke_width = this.line_width ||
      this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 4), 3.0);
    
    var level;
    
    if (this._norm_baseline !== undefined) {
      level = this._graph_top + (this._graph_height - this._norm_baseline * this._graph_height);
      this._d.push();
      this._d.stroke_color  = this.baseline_color;
      this._d.fill_opacity = 0.0;
      // this._d.stroke_dasharray(10, 20);
      this._d.stroke_width = 5;
      this._d.line(this._graph_left, level, this._graph_left + this._graph_width, level);
      this._d.pop();
    }
    
    Bluff.each(this._norm_data, function(data_row) {
      var prev_x = null, prev_y = null;
      
      Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
        if (data_point === undefined) return;
        
        var rad_pos = index * Math.PI * 2 / this._column_count,
            point_distance = data_point * this._radius,
            start_x = this._center_x + Math.sin(rad_pos) * point_distance,
            start_y = this._center_y - Math.cos(rad_pos) * point_distance,
            
            next_index = (index + 1 < data_row[this.klass.DATA_VALUES_INDEX].length) ? index + 1 : 0,
            
            next_rad_pos = next_index * Math.PI * 2 / this._column_count,
            next_point_distance = data_row[this.klass.DATA_VALUES_INDEX][next_index] * this._radius,
            end_x = this._center_x + Math.sin(next_rad_pos) * next_point_distance,
            end_y = this._center_y - Math.cos(next_rad_pos) * next_point_distance;
        
        this._d.stroke = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.line(start_x, start_y, end_x, end_y);
        
        if (!this.hide_dots) this._d.circle(start_x, start_y, start_x - circle_radius, start_y);
      }, this);
      
    }, this);
  },
  
  // the lines connecting in the center, with the first line vertical
  _draw_line_markers: function() {
    if (this.hide_line_markers) return;
    
    // have to do this here (AGAIN)... see draw() in this class
    // because this funtion is called before the @radius, @center_x and @center_y are set
    this._radius = this._graph_height / 2.0;
    this._center_x = this._graph_left + (this._graph_width / 2.0);
    this._center_y = this._graph_top + (this._graph_height / 2.0) - 10; // Move graph up a bit
    
    var rad_pos, marker_label;
    
    for (var index = 0, n = this._column_count; index < n; index++) {
      rad_pos = index * Math.PI * 2 / this._column_count;
      
      // Draw horizontal line markers and annotate with numbers
      this._d.stroke = this.marker_color;
      this._d.stroke_width = 1;
      
      this._d.line(this._center_x, this._center_y, this._center_x + Math.sin(rad_pos) * this._radius, this._center_y - Math.cos(rad_pos) * this._radius);
      
      marker_label = this.labels[index] ? this.labels[index] : '000';
      
      this._draw_label(this._center_x, this._center_y, rad_pos * 360 / (2 * Math.PI), this._radius, marker_label);
    }
  },
  
  _draw_label: function(center_x, center_y, angle, radius, amount) {
    var r_offset = 1.1,
        x_offset = center_x, // + 15 // The label points need to be tweaked slightly
        y_offset = center_y, // + 0  // This one doesn't though
        rad_pos = angle * Math.PI / 180,
        x = x_offset + (radius * r_offset * Math.sin(rad_pos)),
        y = y_offset - (radius * r_offset * Math.cos(rad_pos));
    
    // Draw label
    this._d.fill = this.marker_color;
    if (this.font) this._d.font = this.font;
    this._d.pointsize = this._scale_fontsize(20);
    this._d.stroke = 'transparent';
    this._d.font_weight = 'bold';
    this._d.gravity = 'center';
    this._d.annotate_scaled(0, 0, x, y, amount, this._scale);
  }
});


// Here's how to make a Pie graph:
//
//   g = new Bluff.Pie('canvasId');
//   g.title = "Visual Pie Graph Test";
//   g.data('Fries', 20);
//   g.data('Hamburgers', 50);
//   g.draw();
//
// To control where the pie chart starts creating slices, use #zero_degree.

Bluff.Pie = new JS.Class(Bluff.Base, {
  extend: {
    TEXT_OFFSET_PERCENTAGE: 0.08
  },
  
  // Can be used to make the pie start cutting slices at the top (-90.0)
  // or at another angle. Default is 0.0, which starts at 3 o'clock.
  zero_degreee: null,
  
  // Do not show labels for slices that are less than this percent. Use 0 to always show all labels.
  hide_labels_less_than: null,
  
  initialize_ivars: function() {
    this.callSuper();
    this.zero_degree = 0.0;
    this.hide_labels_less_than = 0.0;
  },
  
  draw: function() {
    this.hide_line_markers = true;
    
    this.callSuper();
    
    if (!this._has_data) return;
    
    var diameter = this._graph_height,
        radius = (Math.min(this._graph_width, this._graph_height) / 2.0) * 0.8,
        top_x = this._graph_left + (this._graph_width - diameter) / 2.0,
        center_x = this._graph_left + (this._graph_width / 2.0),
        center_y = this._graph_top + (this._graph_height / 2.0) - 10, // Move graph up a bit
        total_sum = this._sums_for_pie(),
        prev_degrees = this.zero_degree,
        index = this.klass.DATA_VALUES_INDEX;
    
    // Use full data since we can easily calculate percentages
    if (this.sort) this._data.sort(function(a,b) { return a[index][0] - b[index][0]; });
    Bluff.each(this._data, function(data_row, i) {
      if (data_row[this.klass.DATA_VALUES_INDEX][0] > 0) {
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        
        var current_degrees = (data_row[this.klass.DATA_VALUES_INDEX][0] / total_sum) * 360;
        
        // Gruff uses ellipse() here, but canvas doesn't seem to support it.
        // circle() is fine for our purposes here.
        this._d.circle(center_x, center_y,
                    center_x + radius, center_y,
                    prev_degrees, prev_degrees + current_degrees + 0.5); // <= +0.5 'fudge factor' gets rid of the ugly gaps
        
        var half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2,
            label_val = Math.round((data_row[this.klass.DATA_VALUES_INDEX][0] / total_sum) * 100.0),
            label_string;
        
        if (label_val >= this.hide_labels_less_than) {
          label_string = this._label(data_row[this.klass.DATA_VALUES_INDEX][0]);
          this._draw_label(center_x, center_y, half_angle,
                            radius + (radius * this.klass.TEXT_OFFSET_PERCENTAGE),
                            label_string,
                            data_row, i);
        }
        
        prev_degrees += current_degrees;
      }
    }, this);
    
    // TO_IGNORE_DO debug a circle where the text is drawn...
  },
  
  // Labels are drawn around a slightly wider ellipse to give room for 
  // labels on the left and right.
  _draw_label: function(center_x, center_y, angle, radius, amount, data_row, i) {
    // TO_IGNORE_DO Don't use so many hard-coded numbers
    var r_offset = 20.0,      // The distance out from the center of the pie to get point
        x_offset = center_x,  // + 15.0 # The label points need to be tweaked slightly
        y_offset = center_y,  // This one doesn't though
        radius_offset = radius + r_offset,
        ellipse_factor = radius_offset * 0.15,
        x = x_offset + ((radius_offset + ellipse_factor) * Math.cos(angle * Math.PI/180)),
        y = y_offset + (radius_offset * Math.sin(angle * Math.PI/180));
    
    // Draw label
    this._d.fill = this.font_color;
    if (this.font) this._d.font = this.font;
    this._d.pointsize = this._scale_fontsize(this.marker_font_size);
    this._d.font_weight = 'bold';
    this._d.gravity = 'center';
    this._d.annotate_scaled(0,0, x,y, amount, this._scale);
    
    this._draw_tooltip(x - 20, y - 20, 40, 40,
                       data_row[this.klass.DATA_LABEL_INDEX],
                       data_row[this.klass.DATA_COLOR_INDEX],
                       amount, i);
  },
  
  _sums_for_pie: function() {
    var total_sum = 0;
    Bluff.each(this._data, function(data_row) {
      total_sum += data_row[this.klass.DATA_VALUES_INDEX][0];
    }, this);
    return total_sum;
  }
});


// Graph with individual horizontal bars instead of vertical bars.

Bluff.SideBar = new JS.Class(Bluff.Base, {
  
  // Spacing factor applied between bars
  bar_spacing: 0.9,
  
  draw: function() {
    this.has_left_labels = true;
    this.callSuper();
    
    if (!this._has_data) return;
    this._draw_bars();
  },
  
  _draw_bars: function() {
    this._bars_width       = this._graph_height / this._column_count;
    this._bar_width        = this._bars_width / this._norm_data.length;
    this._d.stroke_opacity = 0.0;
    var height = Bluff.array_new(this._column_count, 0),
        length = Bluff.array_new(this._column_count, this._graph_left),
        padding = (this._bar_width * (1 - this.bar_spacing)) / 2;
    
    Bluff.each(this._norm_data, function(data_row, row_index) {
      var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
      Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
        
        // Using the original calcs from the stacked bar chart
        // to get the difference between
        // part of the bart chart we wish to stack.
        var temp1      = this._graph_left + (this._graph_width - data_point * this._graph_width - height[point_index]),
            temp2      = this._graph_left + this._graph_width - height[point_index],
            difference = temp2 - temp1,
        
            left_x     = length[point_index] - 1,
            left_y     = this._graph_top + (this._bars_width * point_index) + (this._bar_width * row_index) + padding,
            right_x    = left_x + difference,
            right_y    = left_y + this._bar_width * this.bar_spacing;
        
        height[point_index] += (data_point * this._graph_width);
        
        this._d.stroke = 'transparent';
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.rectangle(left_x, left_y, right_x, right_y);
        
        this._draw_tooltip(left_x, left_y,
                           right_x - left_x, right_y - left_y,
                           data_row[this.klass.DATA_LABEL_INDEX],
                           data_row[this.klass.DATA_COLOR_INDEX],
                           raw_data[point_index], point_index);
        
        // Calculate center based on bar_width and current row
        var label_center = this._graph_top + (this._bars_width * point_index + this._bars_width / 2);
        this._draw_label(label_center, point_index);
      }, this)
      
    }, this);
  },
  
  // Instead of base class version, draws vertical background lines and label
  _draw_line_markers: function() {
    
    if (this.hide_line_markers) return;
    
    this._d.stroke_antialias = false;
    
    // Draw horizontal line markers and annotate with numbers
    this._d.stroke_width = 1;
    var number_of_lines = 5;
    
    // TO_IGNORE_DO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
    var increment = this._significant(this._spread / number_of_lines),
        line_diff, x, diff, marker_label;
    for (var index = 0; index <= number_of_lines; index++) {
      
      line_diff    = (this._graph_right - this._graph_left) / number_of_lines;
      x            = this._graph_right - (line_diff * index) - 1;
      diff         = index - number_of_lines;
      marker_label = Math.abs(diff) * increment + this.minimum_value;
      
      this._d.stroke = this.marker_color;
      this._d.line(x, this._graph_bottom, x, this._graph_top);
      
      if (!this.hide_line_numbers) {
        this._d.fill      = this.font_color;
        if (this.font) this._d.font = this.font;
        this._d.stroke    = 'transparent';
        this._d.pointsize = this._scale_fontsize(this.marker_font_size);
        this._d.gravity   = 'center';
        // TO_IGNORE_DO Center text over line
        this._d.annotate_scaled(
                          0, 0, // Width of box to draw text in
                          x, this._graph_bottom + (this.klass.LABEL_MARGIN * 2.0), // Coordinates of text
                          this._label(marker_label), this._scale);
      }
    }
  },
  
  // Draw on the Y axis instead of the X
  _draw_label: function(y_offset, index) {
    if (this.labels[index] && !this._labels_seen[index]) {
      this._d.fill             = this.font_color;
      if (this.font) this._d.font = this.font;
      this._d.stroke           = 'transparent';
      this._d.font_weight      = 'normal';
      this._d.pointsize        = this._scale_fontsize(this.marker_font_size);
      this._d.gravity          = 'east';
      this._d.annotate_scaled(1, 1,
                              this._graph_left - this.klass.LABEL_MARGIN * 2.0, y_offset,
                              this.labels[index], this._scale);
      this._labels_seen[index] = true;
    }
  }
});


// Experimental!!! See also the Net graph.
//
// Submitted by Kevin Clark http://glu.ttono.us/
Bluff.Spider = new JS.Class(Bluff.Base, {
  
  // Hide all text
  hide_text: null,
  hide_axes: null,
  transparent_background: null,
  
  initialize: function(renderer, max_value, target_width) {
    this.callSuper(renderer, target_width);
    this._max_value = max_value;
    this.hide_legend = true;
  },
  
  draw: function() {
    this.hide_line_markers = true;
    
    this.callSuper();
    
    if (!this._has_data) return;
    
    // Setup basic positioning
    var diameter = this._graph_height,
        radius = this._graph_height / 2.0,
        top_x = this._graph_left + (this._graph_width - diameter) / 2.0,
        center_x = this._graph_left + (this._graph_width / 2.0),
        center_y = this._graph_top + (this._graph_height / 2.0) - 25; // Move graph up a bit
    
    this._unit_length = radius / this._max_value;
    
    var total_sum = this._sums_for_spider(),
        prev_degrees = 0.0,
        additive_angle = (2 * Math.PI) / this._data.length,
        
        current_angle = 0.0;
    
    // Draw axes
    if (!this.hide_axes) this._draw_axes(center_x, center_y, radius, additive_angle);
    
    // Draw polygon
    this._draw_polygon(center_x, center_y, additive_angle);
  },
  
  _normalize_points: function(value) {
    return value * this._unit_length;
  },
  
  _draw_label: function(center_x, center_y, angle, radius, amount) {
    var r_offset = 50,            // The distance out from the center of the pie to get point
        x_offset = center_x,      // The label points need to be tweaked slightly
        y_offset = center_y + 0,  // This one doesn't though
        x = x_offset + ((radius + r_offset) * Math.cos(angle)),
        y = y_offset + ((radius + r_offset) * Math.sin(angle));
    
    // Draw label
    this._d.fill = this.marker_color;
    if (this.font) this._d.font = this.font;
    this._d.pointsize = this._scale_fontsize(this.legend_font_size);
    this._d.stroke = 'transparent';
    this._d.font_weight = 'bold';
    this._d.gravity = 'center';
    this._d.annotate_scaled(0, 0,
                            x, y,
                            amount, this._scale);
  },
  
  _draw_axes: function(center_x, center_y, radius, additive_angle, line_color) {
    if (this.hide_axes) return;
    
    var current_angle = 0.0;
    
    Bluff.each(this._data, function(data_row) {
      this._d.stroke = line_color || data_row[this.klass.DATA_COLOR_INDEX];
      this._d.stroke_width = 5.0;
      
      var x_offset = radius * Math.cos(current_angle);
      var y_offset = radius * Math.sin(current_angle);
      
      this._d.line(center_x, center_y,
                   center_x + x_offset,
                   center_y + y_offset);
      
      if (!this.hide_text) this._draw_label(center_x, center_y, current_angle, radius, data_row[this.klass.DATA_LABEL_INDEX]);
      
      current_angle += additive_angle;
    }, this);
  },
  
  _draw_polygon: function(center_x, center_y, additive_angle, color) {
    var points = [],
        current_angle = 0.0;
    Bluff.each(this._data, function(data_row) {
      points.push(center_x + this._normalize_points(data_row[this.klass.DATA_VALUES_INDEX][0]) * Math.cos(current_angle));
      points.push(center_y + this._normalize_points(data_row[this.klass.DATA_VALUES_INDEX][0]) * Math.sin(current_angle));
      current_angle += additive_angle;
    }, this);
    
    this._d.stroke_width = 1.0;
    this._d.stroke = color || this.marker_color;
    this._d.fill = color || this.marker_color;
    this._d.fill_opacity = 0.4;
    this._d.polyline(points);
  },
  
  _sums_for_spider: function() {
    var sum = 0.0;
    Bluff.each(this._data, function(data_row) {
      sum += data_row[this.klass.DATA_VALUES_INDEX][0];
    }, this);
    return sum;
  }
});


// Used by StackedBar and child classes.
Bluff.Base.StackedMixin = new JS.Module({
  // Get sum of each stack
  _get_maximum_by_stack: function() {
    var max_hash = {};
    Bluff.each(this._data, function(data_set) {
      Bluff.each(data_set[this.klass.DATA_VALUES_INDEX], function(data_point, i) {
        if (!max_hash[i]) max_hash[i] = 0.0;
        max_hash[i] += data_point;
      }, this);
    }, this);
    
    // this.maximum_value = 0;
    for (var key in max_hash) {
      if (max_hash[key] > this.maximum_value) this.maximum_value = max_hash[key];
    }
    this.minimum_value = 0;
  }
});


Bluff.StackedArea = new JS.Class(Bluff.Base, {
  include: Bluff.Base.StackedMixin,
  last_series_goes_on_bottom: null,
  
  draw: function() {
    this._get_maximum_by_stack();
    this.callSuper();
    
    if (!this._has_data) return;
    
    this._x_increment = this._graph_width / (this._column_count - 1);
    this._d.stroke = 'transparent';
    
    var height = Bluff.array_new(this._column_count, 0);
    
    var data_points = null;
    var iterator = this.last_series_goes_on_bottom ? 'reverse_each' : 'each';
    Bluff[iterator](this._norm_data, function(data_row) {
      var prev_data_points = data_points;
      data_points = [];
      
      Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
        // Use incremented x and scaled y
        var new_x = this._graph_left + (this._x_increment * index);
        var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height - height[index]);
        
        height[index] += (data_point * this._graph_height);
        
        data_points.push(new_x);
        data_points.push(new_y);
        
        this._draw_label(new_x, index);
      }, this);
      
      var poly_points, i, n;
      
      if (prev_data_points) {
        poly_points = Bluff.array(data_points);
        for (i = prev_data_points.length/2 - 1; i >= 0; i--) {
          poly_points.push(prev_data_points[2*i]);
          poly_points.push(prev_data_points[2*i+1]);
        }
        poly_points.push(data_points[0]);
        poly_points.push(data_points[1]);
      } else {
        poly_points = Bluff.array(data_points);
        poly_points.push(this._graph_right);
        poly_points.push(this._graph_bottom - 1);
        poly_points.push(this._graph_left);
        poly_points.push(this._graph_bottom - 1);
        poly_points.push(data_points[0]);
        poly_points.push(data_points[1]);
      }
      this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
      this._d.polyline(poly_points);
    }, this);
  }
});


Bluff.StackedBar = new JS.Class(Bluff.Base, {
  include: Bluff.Base.StackedMixin,
  
  // Spacing factor applied between bars
  bar_spacing: 0.9,
  
  // Draws a bar graph, but multiple sets are stacked on top of each other.
  draw: function() {
    this._get_maximum_by_stack();
    this.callSuper();
    if (!this._has_data) return;
    
    this._bar_width = this._graph_width / this._column_count;
    var padding = (this._bar_width * (1 - this.bar_spacing)) / 2;
    
    this._d.stroke_opacity = 0.0;
    
    var height = Bluff.array_new(this._column_count, 0);
    
    Bluff.each(this._norm_data, function(data_row, row_index) {
      var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
      
      Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
        // Calculate center based on bar_width and current row
        var label_center = this._graph_left + (this._bar_width * point_index) + (this._bar_width * this.bar_spacing / 2.0);
        this._draw_label(label_center, point_index);
        
        if (data_point == 0) return;
        // Use incremented x and scaled y
        var left_x = this._graph_left + (this._bar_width * point_index) + padding;
        var left_y = this._graph_top + (this._graph_height -
                                        data_point * this._graph_height - 
                                        height[point_index]) + 1;
        var right_x = left_x + this._bar_width * this.bar_spacing;
        var right_y = this._graph_top + this._graph_height - height[point_index] - 1;
        
        // update the total height of the current stacked bar
        height[point_index] += (data_point * this._graph_height);
        
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        this._d.rectangle(left_x, left_y, right_x, right_y);
        
        this._draw_tooltip(left_x, left_y,
                           right_x - left_x, right_y - left_y,
                           data_row[this.klass.DATA_LABEL_INDEX],
                           data_row[this.klass.DATA_COLOR_INDEX],
                           raw_data[point_index], point_index);
      }, this);
    }, this);
  }
});


// A special bar graph that shows a single dataset as a set of
// stacked bars. The bottom bar shows the running total and 
// the top bar shows the new value being added to the array.

Bluff.AccumulatorBar = new JS.Class(Bluff.StackedBar, {
  
  draw: function() {
    if (this._data.length !== 1) throw 'Incorrect number of datasets';
    
    var accumulator_array = [],
        index = 0,
        increment_array = [];
    
    Bluff.each(this._data[0][this.klass.DATA_VALUES_INDEX], function(value) {
      var max = -Infinity;
      Bluff.each(increment_array, function(x) { max = Math.max(max, x); });
      
      increment_array.push((index > 0) ? (value + max) : value);
      accumulator_array.push(increment_array[index] - value);
      index += 1;
    }, this);
    
    this.data("Accumulator", accumulator_array);
    
    this.callSuper();
  }
});


// New gruff graph type added to enable sideways stacking bar charts 
// (basically looks like a x/y flip of a standard stacking bar chart)
//
// alun.eyre@googlemail.com

Bluff.SideStackedBar = new JS.Class(Bluff.SideBar, {
  include: Bluff.Base.StackedMixin,
  
  // Spacing factor applied between bars
  bar_spacing: 0.9,
  
  draw: function() {
    this.has_left_labels = true;
    this._get_maximum_by_stack();
    this.callSuper();
  },
  
  _draw_bars: function() {
    this._bar_width = this._graph_height / this._column_count;
    var height = Bluff.array_new(this._column_count, 0),
        length = Bluff.array_new(this._column_count, this._graph_left),
        padding = (this._bar_width * (1 - this.bar_spacing)) / 2;

    Bluff.each(this._norm_data, function(data_row, row_index) {
      var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
      
      Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
        
        // using the original calcs from the stacked bar chart to get the difference between
        // part of the bart chart we wish to stack.
        var temp1 = this._graph_left + (this._graph_width -
                                            data_point * this._graph_width - 
                                            height[point_index]) + 1;
        var temp2 = this._graph_left + this._graph_width - height[point_index] - 1;
        var difference = temp2 - temp1;
        
        this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
        
        var left_x = length[point_index], //+ 1
            left_y = this._graph_top + (this._bar_width * point_index) + padding,
            right_x = left_x + difference,
            right_y = left_y + this._bar_width * this.bar_spacing;
        length[point_index] += difference;
        height[point_index] += (data_point * this._graph_width - 2);
        
        this._d.rectangle(left_x, left_y, right_x, right_y);
        
        this._draw_tooltip(left_x, left_y,
                           right_x - left_x, right_y - left_y,
                           data_row[this.klass.DATA_LABEL_INDEX],
                           data_row[this.klass.DATA_COLOR_INDEX],
                           raw_data[point_index], point_index);
        
        // Calculate center based on bar_width and current row
        var label_center = this._graph_top + (this._bar_width * point_index) + (this._bar_width * this.bar_spacing / 2.0);
        this._draw_label(label_center, point_index);
      }, this);
    }, this);
  },
  
  _larger_than_max: function(data_point, index) {
    index = index || 0;
    return this._max(data_point, index) > this.maximum_value;
  },
  
  _max: function(data_point, index) {
    var sum = 0;
    Bluff.each(this._data, function(item) {
      sum += item[this.klass.DATA_VALUES_INDEX][index];
    }, this);
    return sum;
  }
});


Bluff.Mini.Legend = new JS.Module({
  
  hide_mini_legend: false,
  
  // The canvas needs to be bigger so we can put the legend beneath it.
  _expand_canvas_for_vertical_legend: function() {
    if (this.hide_mini_legend) return;
    
    this._legend_labels = Bluff.map(this._data, function(item) {
      return item[this.klass.DATA_LABEL_INDEX];
    }, this);
    
    var legend_height = this._scale_fontsize(
                          this._data.length * this._calculate_line_height() +
                          this.top_margin + this.bottom_margin);
    
    this._original_rows = this._raw_rows;
    this._original_columns = this._raw_columns;
    
    switch (this.legend_position) {
      case 'right':
        this._rows = Math.max(this._rows, legend_height);
        this._columns += this._calculate_legend_width() + this.left_margin;
        break;
      
      default:
        this._rows += legend_height;
        break;
    }
    this._render_background();
  },
  
  _calculate_line_height: function() {
    return this._calculate_caps_height(this.legend_font_size) * 1.7;
  },
  
  _calculate_legend_width: function() {
    var width = 0;
    Bluff.each(this._legend_labels, function(label) {
      width = Math.max(this._calculate_width(this.legend_font_size, label), width);
    }, this);
    return this._scale_fontsize(width + 40*1.7);
  },
  
  // Draw the legend beneath the existing graph.
  _draw_vertical_legend: function() {
    if (this.hide_mini_legend) return;
    
    var legend_square_width = 40.0, // small square with color of this item
        legend_square_margin = 10.0,
        legend_left_margin = 100.0,
        legend_top_margin = 40.0;
    
    // May fix legend drawing problem at small sizes
    if (this.font) this._d.font = this.font;
    this._d.pointsize = this.legend_font_size;
    
    var current_x_offset, current_y_offset;
    
    switch (this.legend_position) {
      case 'right':
        current_x_offset = this._original_columns + this.left_margin;
        current_y_offset = this.top_margin + legend_top_margin;
        break;
      
      default:
        current_x_offset = legend_left_margin,
        current_y_offset = this._original_rows + legend_top_margin;
        break;
    }
    
    this._debug(function() {
      this._d.line(0.0, current_y_offset, this._raw_columns, current_y_offset);
    });
    
    Bluff.each(this._legend_labels, function(legend_label, index) {
      
      // Draw label
      this._d.fill = this.font_color;
      if (this.font) this._d.font = this.font;
      this._d.pointsize = this._scale_fontsize(this.legend_font_size);
      this._d.stroke = 'transparent';
      this._d.font_weight = 'normal';
      this._d.gravity = 'west';
      this._d.annotate_scaled(this._raw_columns, 1.0,
                        current_x_offset + (legend_square_width * 1.7), current_y_offset, 
                        this._truncate_legend_label(legend_label), this._scale);
      
      // Now draw box with color of this dataset
      this._d.stroke = 'transparent';
      this._d.fill = this._data[index][this.klass.DATA_COLOR_INDEX];
      this._d.rectangle(current_x_offset, 
                        current_y_offset - legend_square_width / 2.0, 
                        current_x_offset + legend_square_width, 
                        current_y_offset + legend_square_width / 2.0);
      
      current_y_offset += this._calculate_line_height();
    }, this);
    this._color_index = 0;
  },
  
  // Shorten long labels so they will fit on the canvas.
  _truncate_legend_label: function(label) {
    var truncated_label = String(label);
    while (this._calculate_width(this._scale_fontsize(this.legend_font_size), truncated_label) > (this._columns - this.legend_left_margin - this.right_margin) && (truncated_label.length > 1))
      truncated_label = truncated_label.substr(0, truncated_label.length-1);
    return truncated_label + (truncated_label.length < String(label).length ? "..." : '');
  }
});


// Makes a small bar graph suitable for display at 200px or even smaller.
//
Bluff.Mini.Bar = new JS.Class(Bluff.Bar, {
  include: Bluff.Mini.Legend,
  
  initialize_ivars: function() {
    this.callSuper();
    
    this.hide_legend = true;
    this.hide_title = true;
    this.hide_line_numbers = true;
    
    this.marker_font_size = 50.0;
    this.minimum_value = 0.0;
    this.maximum_value = 0.0;
    this.legend_font_size = 60.0;
  },
  
  draw: function() {
    this._expand_canvas_for_vertical_legend();
    
    this.callSuper();
    
    this._draw_vertical_legend();
  }
});


// Makes a small pie graph suitable for display at 200px or even smaller.
//
Bluff.Mini.Pie = new JS.Class(Bluff.Pie, {
  include: Bluff.Mini.Legend,
  
  initialize_ivars: function() {
    this.callSuper();
    
    this.hide_legend = true;
    this.hide_title = true;
    this.hide_line_numbers = true;
    
    this.marker_font_size = 60.0;
    this.legend_font_size = 60.0;
  },
  
  draw: function() {
    this._expand_canvas_for_vertical_legend();
    
    this.callSuper();
    
    this._draw_vertical_legend();
  }
});


// Makes a small pie graph suitable for display at 200px or even smaller.
//
Bluff.Mini.SideBar = new JS.Class(Bluff.SideBar, {
  include: Bluff.Mini.Legend,
  
  initialize_ivars: function() {
    this.callSuper();
    this.hide_legend = true;
    this.hide_title = true;
    this.hide_line_numbers = true;
    
    this.marker_font_size = 50.0;
    this.legend_font_size = 50.0;
  },
  
  draw: function() {
    this._expand_canvas_for_vertical_legend();
    
    this.callSuper();
    
    this._draw_vertical_legend();
  }
});


Bluff.Renderer = new JS.Class({
  extend: {
    WRAPPER_CLASS:  'bluff-wrapper',
    TEXT_CLASS:     'bluff-text',
    TARGET_CLASS:   'bluff-tooltip-target'
  },

  font:     'Arial, Helvetica, Verdana, sans-serif',
  gravity:  'north',
  
  initialize: function(canvasId) {
    this._canvas = document.getElementById(canvasId);
    this._ctx = this._canvas.getContext('2d');
  },
  
  scale: function(sx, sy) {
    this._sx = sx;
    this._sy = sy || sx;
  },
  
  caps_height: function(font_size) {
    var X = this._sized_text(font_size, 'X'),
        height = this._element_size(X).height;
    this._remove_node(X);
    return height;
  },
  
  text_width: function(font_size, text) {
    var element = this._sized_text(font_size, text);
    var width = this._element_size(element).width;
    this._remove_node(element);
    return width;
  },
  
  get_type_metrics: function(text) {
    var node = this._sized_text(this.pointsize, text);
    document.body.appendChild(node);
    var size = this._element_size(node);
    this._remove_node(node);
    return size;
  },
  
  clear: function(width, height) {
    this._canvas.width = width;
    this._canvas.height = height;
    this._ctx.clearRect(0, 0, width, height);
    var wrapper = this._text_container(), children = wrapper.childNodes, i = children.length;
    wrapper.style.width = width + 'px';
    wrapper.style.height = height + 'px';
    while (i--) {
      if (children[i].tagName.toLowerCase() !== 'canvas') {
        Bluff.Event.stopObserving(children[i]);
        this._remove_node(children[i]);
      }
    }
  },
  
  push: function() {
    this._ctx.save();
  },
  
  pop: function() {
    this._ctx.restore();
  },
  
  render_gradiated_background: function(width, height, top_color, bottom_color) {
    this.clear(width, height);
    var gradient = this._ctx.createLinearGradient(0,0, 0,height);
    gradient.addColorStop(0, top_color);
    gradient.addColorStop(1, bottom_color);
    this._ctx.fillStyle = gradient;
    this._ctx.fillRect(0, 0, width, height);
  },
  
  render_solid_background: function(width, height, color) {
    this.clear(width, height);
    this._ctx.fillStyle = color;
    this._ctx.fillRect(0, 0, width, height);
  },
  
  annotate_scaled: function(width, height, x, y, text, scale) {
    var scaled_width = (width * scale) >= 1 ? (width * scale) : 1;
    var scaled_height = (height * scale) >= 1 ? (height * scale) : 1;
    var text = this._sized_text(this.pointsize, text);
    text.style.color = this.fill;
    text.style.cursor = 'default';
    text.style.fontWeight = this.font_weight;
    text.style.textAlign = 'center';
    text.style.left = (this._sx * x + this._left_adjustment(text, scaled_width)) + 'px';
    text.style.top = (this._sy * y + this._top_adjustment(text, scaled_height)) + 'px';
  },
  
  tooltip: function(left, top, width, height, name, color, data) {
    if (width < 0) left += width;
    if (height < 0) top += height;
    
    var wrapper = this._canvas.parentNode,
        target = document.createElement('div');
    target.className = this.klass.TARGET_CLASS;
    target.style.cursor = 'default';
    target.style.position = 'absolute';
    target.style.left = (this._sx * left - 3) + 'px';
    target.style.top = (this._sy * top - 3) + 'px';
    target.style.width = (this._sx * Math.abs(width) + 5) + 'px';
    target.style.height = (this._sy * Math.abs(height) + 5) + 'px';
    target.style.fontSize = 0;
    target.style.overflow = 'hidden';
    
    Bluff.Event.observe(target, 'mouseover', function(node) {
      Bluff.Tooltip.show(name, color, data);
    });
    Bluff.Event.observe(target, 'mouseout', function(node) {
      Bluff.Tooltip.hide();
    });
    
    wrapper.appendChild(target);
    return target;
  },
  
  circle: function(origin_x, origin_y, perim_x, perim_y, arc_start, arc_end) {
    var radius = Math.sqrt(Math.pow(perim_x - origin_x, 2) + Math.pow(perim_y - origin_y, 2));
    var alpha = 0, beta = 2 * Math.PI; // radians to full circle
    
    this._ctx.fillStyle = this.fill;
    this._ctx.beginPath();
    
    if (arc_start !== undefined && arc_end !== undefined &&
        Math.abs(Math.floor(arc_end - arc_start)) !== 360) {
      alpha = arc_start * Math.PI/180;
      beta  = arc_end   * Math.PI/180;
      
      this._ctx.moveTo(this._sx * (origin_x + radius * Math.cos(beta)), this._sy * (origin_y + radius * Math.sin(beta)));
      this._ctx.lineTo(this._sx * origin_x, this._sy * origin_y);
      this._ctx.lineTo(this._sx * (origin_x + radius * Math.cos(alpha)), this._sy * (origin_y + radius * Math.sin(alpha)));
    }
    this._ctx.arc(this._sx * origin_x, this._sy * origin_y, this._sx * radius, alpha, beta, false); // draw it clockwise
    this._ctx.fill();
  },
  
  line: function(sx, sy, ex, ey) {
    this._ctx.strokeStyle = this.stroke;
    this._ctx.lineWidth = this.stroke_width;
    this._ctx.beginPath();
    this._ctx.moveTo(this._sx * sx, this._sy * sy);
    this._ctx.lineTo(this._sx * ex, this._sy * ey);
    this._ctx.stroke();
  },
  
  polyline: function(points) {
    this._ctx.fillStyle = this.fill;
    this._ctx.globalAlpha = this.fill_opacity || 1;
    try { this._ctx.strokeStyle = this.stroke; } catch (e) {}
    var x = points.shift(), y = points.shift();
    this._ctx.beginPath();
    this._ctx.moveTo(this._sx * x, this._sy * y);
    while (points.length > 0) {
      x = points.shift(); y = points.shift();
      this._ctx.lineTo(this._sx * x, this._sy * y);
    }
    this._ctx.fill();
  },
  
  rectangle: function(ax, ay, bx, by) {
    var temp;
    if (ax > bx) { temp = ax; ax = bx; bx = temp; }
    if (ay > by) { temp = ay; ay = by; by = temp; }
    try {
      this._ctx.fillStyle = this.fill;
      this._ctx.fillRect(this._sx * ax, this._sy * ay, this._sx * (bx-ax), this._sy * (by-ay));
    } catch (e) {}
    try {
      this._ctx.strokeStyle = this.stroke;
      if (this.stroke !== 'transparent')
        this._ctx.strokeRect(this._sx * ax, this._sy * ay, this._sx * (bx-ax), this._sy * (by-ay));
    } catch (e) {}
  },
  
  _left_adjustment: function(node, width) {
    var w = this._element_size(node).width;
    switch (this.gravity) {
      case 'west':    return 0;
      case 'east':    return width - w;
      case 'north': case 'south': case 'center':
        return (width - w) / 2;
    }
  },
  
  _top_adjustment: function(node, height) {
    var h = this._element_size(node).height;
    switch (this.gravity) {
      case 'north':   return 0;
      case 'south':   return height - h;
      case 'west': case 'east': case 'center':
        return (height - h) / 2;
    }
  },
  
  _text_container: function() {
    var wrapper = this._canvas.parentNode;
    if (wrapper.className === this.klass.WRAPPER_CLASS) return wrapper;
    wrapper = document.createElement('div');
    wrapper.className = this.klass.WRAPPER_CLASS;
    
    wrapper.style.position = 'relative';
    wrapper.style.border = 'none';
    wrapper.style.padding = '0 0 0 0';
    
    this._canvas.parentNode.insertBefore(wrapper, this._canvas);
    wrapper.appendChild(this._canvas);
    return wrapper;
  },
  
  _sized_text: function(size, content) {
    var text = this._text_node(content);
    text.style.fontFamily = this.font;
    text.style.fontSize = (typeof size === 'number') ? size + 'px' : size;
    return text;
  },
  
  _text_node: function(content) {
    var div = document.createElement('div');
    div.className = this.klass.TEXT_CLASS;
    div.style.position = 'absolute';
    div.appendChild(document.createTextNode(content));
    this._text_container().appendChild(div);
    return div;
  },
  
  _remove_node: function(node) {
    node.parentNode.removeChild(node);
    if (node.className === this.klass.TARGET_CLASS)
      Bluff.Event.stopObserving(node);
  },
  
  _element_size: function(element) {
    var display = element.style.display;
    return (display && display !== 'none')
        ? {width: element.offsetWidth, height: element.offsetHeight}
        : {width: element.clientWidth, height: element.clientHeight};
  }
});


// DOM event module, adapted from Prototype
// Copyright (c) 2005-2008 Sam Stephenson

Bluff.Event = {
  _cache: [],
  
  _isIE: (window.attachEvent && navigator.userAgent.indexOf('Opera') === -1),
  
  observe: function(element, eventName, callback, scope) {
    var handlers = Bluff.map(this._handlersFor(element, eventName),
                      function(entry) { return entry._handler });
    if (Bluff.index(handlers, callback) !== -1) return;
    
    var responder = function(event) {
      callback.call(scope || null, element, Bluff.Event._extend(event));
    };
    this._cache.push({_node: element, _name: eventName,
                      _handler: callback, _responder: responder});
    
    if (element.addEventListener)
      element.addEventListener(eventName, responder, false);
    else
      element.attachEvent('on' + eventName, responder);
  },
  
  stopObserving: function(element) {
    var handlers = element ? this._handlersFor(element) : this._cache;
    Bluff.each(handlers, function(entry) {
      if (entry._node.removeEventListener)
        entry._node.removeEventListener(entry._name, entry._responder, false);
      else
        entry._node.detachEvent('on' + entry._name, entry._responder);
    });
  },
  
  _handlersFor: function(element, eventName) {
    var results = [];
    Bluff.each(this._cache, function(entry) {
      if (element && entry._node !== element) return;
      if (eventName && entry._name !== eventName) return;
      results.push(entry);
    });
    return results;
  },
  
  _extend: function(event) {
    if (!this._isIE) return event;
    if (!event) return false;
    if (event._extendedByBluff) return event;
    event._extendedByBluff = true;
    
    var pointer = this._pointer(event);
    event.target = event.srcElement;
    event.pageX = pointer.x;
    event.pageY = pointer.y;
    
    return event;
  },
  
  _pointer: function(event) {
    var docElement = document.documentElement,
        body = document.body || { scrollLeft: 0, scrollTop: 0 };
    return {
      x: event.pageX || (event.clientX +
                        (docElement.scrollLeft || body.scrollLeft) -
                        (docElement.clientLeft || 0)),
      y: event.pageY || (event.clientY +
                        (docElement.scrollTop || body.scrollTop) -
                        (docElement.clientTop || 0))
    };
  }
};

if (Bluff.Event._isIE)
  window.attachEvent('onunload', function() {
    Bluff.Event.stopObserving();
    Bluff.Event._cache = null;
  });

if (navigator.userAgent.indexOf('AppleWebKit/') > -1)
  window.addEventListener('unload', function() {}, false);


Bluff.Tooltip = new JS.Singleton({
  LEFT_OFFSET:  20,
  TOP_OFFSET:   -6,
  DATA_LENGTH:  8,
  
  CLASS_NAME:   'bluff-tooltip',
  
  setup: function() {
    this._tip = document.createElement('div');
    this._tip.className = this.CLASS_NAME;
    this._tip.style.position = 'absolute';
    this.hide();
    document.body.appendChild(this._tip);
    
    Bluff.Event.observe(document.body, 'mousemove', function(body, event) {
      this._tip.style.left = (event.pageX + this.LEFT_OFFSET) + 'px';
      this._tip.style.top = (event.pageY + this.TOP_OFFSET) + 'px';
    }, this);
  },
  
  show: function(name, color, data) {
    data = Number(String(data).substr(0, this.DATA_LENGTH));
    this._tip.innerHTML = '<span class="color" style="background: ' + color + ';">&nbsp;</span> ' +
                          '<span class="label">' + name + '</span> ' +
                          '<span class="data">' + data + '</span>';
    this._tip.style.display = '';
  },
  
  hide: function() {
    this._tip.style.display = 'none';
  }
});

Bluff.Event.observe(window, 'load', Bluff.Tooltip.method('setup'));


Bluff.TableReader = new JS.Class({
  
  NUMBER_FORMAT: /\-?(0|[1-9]\d*)(\.\d+)?(e[\+\-]?\d+)?/i,
  
  initialize: function(table, options) {
    this._options = options || {};
    this._orientation = this._options.orientation || 'auto';
    
    this._table = (typeof table === 'string')
        ? document.getElementById(table)
        : table;
  },
  
  // Get array of data series from the table
  get_data: function() {
    if (!this._data) this._read();
    return this._data;
  },
  
  // Get set of axis labels to use for the graph
  get_labels: function() {
    if (!this._labels) this._read();
    return this._labels;
  },
  
  // Get the title from the table's caption
  get_title: function() {
    return this._title;
  },
  
  // Return series number i
  get_series: function(i) {
    if (this._data[i]) return this._data[i];
    return this._data[i] = {points: []};
  },
  
  // Gather data by reading from the table
  _read: function() {
    this._row = this._col = 0;
    this._row_offset = this._col_offset = 0;
    this._data = [];
    this._labels = {};
    this._row_headings = [];
    this._col_headings = [];
    this._skip_rows = [];
    this._skip_cols = [];
    
    this._walk(this._table);
    this._cleanup();
    this._orient();
    
    Bluff.each(this._col_headings, function(heading, i) {
      this.get_series(i - this._col_offset).name = heading;
    }, this);
    
    Bluff.each(this._row_headings, function(heading, i) {
      this._labels[i - this._row_offset] = heading;
    }, this);
  },
  
  // Walk the table's DOM tree
  _walk: function(node) {
    this._visit(node);
    var i, children = node.childNodes, n = children.length;
    for (i = 0; i < n; i++) this._walk(children[i]);
  },
  
  // Read a single DOM node from the table
  _visit: function(node) {
    if (!node.tagName) return;
    var content = this._strip_tags(node.innerHTML), x, y;
    switch (node.tagName.toUpperCase()) {
    
      case 'TR':
        if (!this._has_data) this._row_offset = this._row;
        this._row += 1;
        this._col = 0;
        break;
      
      case 'TD':
        if (!this._has_data) this._col_offset = this._col;
        this._has_data = true;
        this._col += 1;
        content = content.match(this.NUMBER_FORMAT);
        if (content === null) {
          this.get_series(x).points[y] = null;
        } else {
          x = this._col - this._col_offset - 1;
          y = this._row - this._row_offset - 1;
          this.get_series(x).points[y] = parseFloat(content[0]);
        }
        break;
      
      case 'TH':
        this._col += 1;
        if (this._ignore(node)) {
          this._skip_cols.push(this._col);
          this._skip_rows.push(this._row);
        }
        if (this._col === 1 && this._row === 1)
          this._row_headings[0] = this._col_headings[0] = content;
        else if (node.scope === "row" || this._col === 1)
          this._row_headings[this._row - 1] = content;
        else
          this._col_headings[this._col - 1] = content;
        break;
      
      case 'CAPTION':
        this._title = content;
        break;
    }
  },
  
  _ignore: function(node) {
    if (!this._options.except) return false;
    
    var content = this._strip_tags(node.innerHTML),
        classes = (node.className || '').split(/\s+/),
        list = [].concat(this._options.except);
    
    if (Bluff.index(list, content) >= 0) return true;
    var i = classes.length;
    while (i--) {
      if (Bluff.index(list, classes[i]) >= 0) return true;
    }
    return false;
  },
  
  _cleanup: function() {
    var i = this._skip_cols.length, index;
    while (i--) {
      index = this._skip_cols[i];
      if (index <= this._col_offset) continue;
      this._col_headings.splice(index - 1, 1);
      if (index >= this._col_offset)
        this._data.splice(index - 1 - this._col_offset, 1);
    }
    
    var i = this._skip_rows.length, index;
    while (i--) {
      index = this._skip_rows[i];
      if (index <= this._row_offset) continue;
      this._row_headings.splice(index - 1, 1);
      Bluff.each(this._data, function(series) {
        if (index >= this._row_offset)
          series.points.splice(index - 1 - this._row_offset, 1);
      }, this);
    }
  },
  
  _orient: function() {
    switch (this._orientation) {
      case 'auto':
        if ((this._row_headings.length > 1 && this._col_headings.length === 1) ||
            this._row_headings.length < this._col_headings.length) {
          this._transpose();
        }
        break;
        
      case 'rows':
        this._transpose();
        break;
    }
  },
  
  // Transpose data in memory
  _transpose: function() {
    var data = this._data, tmp;
    this._data = [];
    
    Bluff.each(data, function(row, i) {
      Bluff.each(row.points, function(point, p) {
        this.get_series(p).points[i] = point;
      }, this);
    }, this);
    
    tmp = this._row_headings;
    this._row_headings = this._col_headings;
    this._col_headings = tmp;
    
    tmp = this._row_offset;
    this._row_offset = this._col_offset;
    this._col_offset = tmp;
  },
  
  // Remove HTML from a string
  _strip_tags: function(string) {
    return string.replace(/<\/?[^>]+>/gi, '');
  },
  
  extend: {
    Mixin: new JS.Module({
      data_from_table: function(table, options) {
        var reader    = new Bluff.TableReader(table, options),
            data_rows = reader.get_data();
        
        Bluff.each(data_rows, function(row) {
          this.data(row.name, row.points);
        }, this);
        
        this.labels = reader.get_labels();
        this.title  = reader.get_title() || this.title;
      }
    })
  }
});

Bluff.Base.include(Bluff.TableReader.Mixin);