// Bubbles for RaphaelJS 1.2.4
// requires jQuery 1.4
// Raphael.bubbleGraph needs data, color and a label (sorta like in flot.js) [{data: [[x1,y1,r1]], color:'#DBFF00', label: 'wondertwins activate!'}, {data: [[x2,y2,r2], [x3,y3,r3]], color:'#5F3800', label: 'shape of an eagle'} ]
// ex:
//  paper.bubbleGraph(data, {height: , width:, hover:[in_fn,out_fn], click:fn})

    function Bubbles(bubble_array, paper, opts) {
      var bubbles = this;
      bubbles.colors = ['#2f80fb', '#ff5f88', '#ff9500', '#ffa038', '#00b6cc', '#cc3300', '#2f2ea2', 
      '#9900ff', '#aaaa99', '#6c891e', '#b71ef2', '#7681A9', '#CC9A00', '#43DBFC', 
      '#FC4366', '#8CA466'];
      bubbles.height       = opts.height; // because ie can suck it
      bubbles.width        = opts.width; // like for real, ie is not fun
      bubbles.font_size    = opts.font_size || 11;
      bubbles.font_color   = opts.font_color;
      bubbles.maximum_size = 64; // TODO opt
      bubbles.minimum_size = 4; // TODO opt
      bubbles.padding      = bubbles.width * 0.1; // 10% padding
      bubbles.label_length = opts.label_length || 10
      bubbles.border_width = opts.border_width || 3; 
      bubbles.border_color = opts.borderColor || '#7d7d7d'; 
      bubbles.onClick      = opts.click; //opt uses jquery
      bubbles.mouseIn      = opts.hover[0];
      bubbles.mouseOut     = opts.hover[1];
      
      bubbles.addPoint     = function (point){
                               log('add', point.label, point.data);
                               //convert point and scale it to this graph
                               paper.clear();
                               series.push(point);
                               scale_sort_and_format()
                             };
      bubbles.reorderBubbles = function(){
                                 series.bubbles_sort_by('z', 'DESC');
                                 $.each(series, function(i,point){
                                   point.circle.toFront();
                                 });
                               };
      // load up the data 
      // TODO this does not work for situations where I have two series of 5 bubbles each
      // right now i have 1 series per bubble - which isn't ideal
      var series = [];
      $.each(bubble_array, function(index,point) {
        if(point){
           series.push(point);
        }
      });
      bubbles.series = series;
      scale_sort_and_format();
      
      function draw_and_label_axes(){
        // draw x and y axis
        var axis_width = bubbles.width-15
        var axis_height = bubbles.height-15
        var axis_color = '#aaa'
        // y axis
        bubbles.y_axis = paper.path( "M15 15L15 "+axis_height+'');
        bubbles.y_axis.attr({'stroke-width': 1, 'stroke': axis_color, 'opacity': 0.5 });
        // y label
        bubbles.y_axis_label = paper.text(10,axis_height/2, 'cohesiveness');
        bubbles.y_axis_label.attr( {'font-weight': 'bold', 'font-size': bubbles.font_size, fill: axis_color})
        bubbles.y_axis_label.rotate(90);
        // x axis
        bubbles.x_axis = paper.path( "M15 "+axis_height+"L"+axis_width+" " + axis_height);
        bubbles.x_axis.attr({'stroke-width': 1, 'stroke': axis_color, 'opacity': 0.5 });
        // x min label
        bubbles.x_axis_min_label = paper.text(15, axis_height+5, 'emerging');
        bubbles.x_axis_min_label.attr( {'font-weight': 'bold', 'font-size': bubbles.font_size, fill: axis_color, 'text-anchor': 'start'})
        // x max label
        bubbles.x_axis_max_label = paper.text(15, axis_height+5, 'emerging');
        bubbles.x_axis_max_label.attr( {'font-weight': 'bold', 'font-size': bubbles.font_size, fill: axis_color, 'text-anchor': 'start'})
        // x min value
        bubbles.x_axis_min = paper.text(20, axis_height-6, bubbles.range.x.min.toFixed(2));
        bubbles.x_axis_min.attr({'font-size': 8, 'fill': axis_color, 'text-anchor': 'start'} )
        // x max value
        bubbles.x_axis_max = paper.text(axis_width-2, axis_height-6, bubbles.range.x.max.toFixed(2));
        bubbles.x_axis_max.attr({'font-size': 8, 'fill': axis_color, 'text-anchor': 'start'} )
        // y min value
        bubbles.y_axis_min = paper.text(10, 15, bubbles.range.y.min.toFixed(2));
        bubbles.y_axis_min.attr({'font-size': 8, 'fill': axis_color, 'text-anchor': 'start'} )
        bubbles.y_axis_min.rotate(90);
        // y max value
        bubbles.y_axis_max = paper.text(10, axis_height-7, bubbles.range.y.max.toFixed(2));
        bubbles.y_axis_max.attr({'font-size': 8, 'fill': axis_color, 'text-anchor': 'start'} )
        bubbles.y_axis_max.rotate(90);
      };


      
      function scale_sort_and_format(){
        if(bubbles.series.length>0){
          $.each(series, function(index, point){
           setup_point(point, index);
          });
          setup_scaling();
          series.bubbles_sort_by('z', 'DESC'); //reset sort to z to laydown biggest bubbles first
          bubbles.label_positions = [];
          $.each(series, function(index, point){
            format_point(point);
          });
        }
      }
      
      function setup_point(point,index){
        if(!point.color){
          point.color = bubbles.colors[index%(bubbles.colors.length)];
        }
        point.x = point.data[0];
        point.y = point.data[1];
        point.z = point.data[2];
        point.size = point.z;
      }
      
      function format_point(point){
        point.x = ((point.x - bubbles.range.x.min)*bubbles.x_scale) + bubbles.padding + bubbles.maximum_size;
        point.y = ((point.y - bubbles.range.y.min)*bubbles.y_scale) + bubbles.padding + bubbles.maximum_size;
        point.z = ((point.z - bubbles.range.z.min)*bubbles.z_scale) + bubbles.minimum_size;
        point.label_y = (point.y < bubbles.height/2) ? bubbles.padding : bubbles.height - bubbles.padding;
        bubbles.label_positions.push(point);

        // flip the label_y if its inside the bubble
        var label_to_y = point.label_y < point.y ? (point.label_y - point.y) : (point.y - point.label_y);
        if(label_to_y < point.z){
           point.label_y = (point.label_y == bubbles.padding) ?  bubbles.height - bubbles.padding : bubbles.padding;
         }

        //try to place the label so they don't overlap each other
        x_pad = bubbles.font_size;
        $.each(bubbles.label_positions, function(index, p) {
          if(p!=point){
            if(p.x < (point.x + x_pad) && p.x > (point.x - x_pad)){
              if(point.label_y < (p.label_y + x_pad) && point.label_y > (p.label_y - x_pad)){
                point.label_y = (point.label_y == bubbles.padding) ?  bubbles.height - bubbles.padding : bubbles.padding;
              }
            }
          }
        });

        render_bubbles(point);
        
        // attach the hover and click mouse events to the circle and text
        $(point.circle.node).hover(function(e){bubbles.mouseIn(e, point, series, bubbles);}, function(e){ bubbles.mouseOut(e, point, series, bubbles);});
        $(point.circle.node).click(function(e){bubbles.onClick(e, point, series, bubbles);});
        $(point.text_label.node).click(function(e){bubbles.onClick(e,point,series, bubbles);});
        $(point.circle.node).css({cursor: 'pointer'});
        $(point.text_label.node).css({cursor: 'pointer'});
      }

      function setup_scaling(){
        bubbles.range     = series_range(series);
        padding           = bubbles.padding + bubbles.maximum_size;
        x_width           = bubbles.width  - padding * 2; //new width to include padding and make sure the largest bubble fits
        y_width           = bubbles.height - padding * 2;  
        z_width           = bubbles.maximum_size - bubbles.minimum_size;
        z_range           = (bubbles.range.z.max - bubbles.range.z.min)
        bubbles.max_z     = bubbles.range.z.max;
        bubbles.z_scale   = z_range > 0 ? z_width/z_range : z_width
        bubbles.x_scale   = x_width/(bubbles.range.x.max - bubbles.range.x.min);
        bubbles.y_scale   = y_width/(bubbles.range.y.max - bubbles.range.y.min);
      }
      
      function series_range(series){
        $.each(series, function(index, point){ point.x_order = index;});
        bubbles.range = { x: series.bubbles_min_max('x')};
        bubbles.range.y = series.bubbles_min_max('y');
        bubbles.range.z = series.bubbles_min_max('z');
        return bubbles.range;
      }
      
      function render_bubbles(point){
        // lay down the line to the term label
        point.label_line = paper.path( "M" + point.x + " " +  point.y + "V" + point.label_y );
        point.label_line.attr( 'stroke-width', 1 );
        point.label_line.attr( 'stroke', bubbles.font_color || point.color );
        point.label_line.attr( 'opacity', 0.5 );

        // lay down the shadow
        var shadow_padding = point.z*0.1 > 3 ? point.z*0.1 : 4 ;
        point.shadow = paper.circle( point.x+shadow_padding, point.y+shadow_padding, point.z );
        point.shadow.attr( 'stroke-width', 0 );
        point.shadow.attr( 'fill', '#aaa' );      
        point.shadow.attr( 'opacity', 0.2);

        // Render base circle that is full color, no gradient, mostly trasparent   
        point.base_circle = paper.circle( point.x, point.y, point.z );
        point.base_circle.attr( 'stroke-width', 3);
        point.base_circle.attr( 'stroke', '#fff' );
        point.base_circle.attr( 'fill', point.color );      
        point.base_circle.attr( 'opacity', 0.3 );

        // Render overlay circle that has gradient and is mostly transparent
        // hack to make gradient fill from color to a lightened version of its color
        point.circle = paper.circle( point.x, point.y, point.z );
        point.circle.attr( 'stroke-width', 3 );
        point.circle.attr( 'stroke', bubbles.border_color );
        // SVG-Gradient: ‹angle›-‹colour›[-‹colour›[:‹offset›]]*-‹colour›
        point.circle.attr( 'fill', point.color );      
        point.circle.attr( 'gradient', '315-' + point.color + '-#fff');
        point.circle.attr( 'opacity', 0.6 );
        
        // Render the label at 45 degree angle
        label_padding = 2;
        point.label_y = point.label_y > point.y ? point.label_y + label_padding : point.label_y - label_padding;
        point.text_label = paper.text(point.x, point.label_y, point.label.bubbles_truncate(bubbles.label_length));
        point.text_label.attr( 'font-weight', 'bold' );
        point.text_label.attr( 'font-size', bubbles.font_size );  
        point.text_label.attr( 'fill', bubbles.font_color || point.color );
        point.label_y > point.y ? point.text_label.attr( 'text-anchor', 'start' ) : point.text_label.attr( 'text-anchor', 'end' );
        point.text_label.rotate(45,point.x, point.label_y);
        
        point.label_line.toFront();
        return;
      }

      function parse_options(opts) {
          if (opts.border_color)
          bubbles.border_color = opts.border_color;
      }

      function log() {if (window.console) {console.log.apply(console, arguments);}}
      
    }//end of bubbles

  
  Raphael.fn.bubbleGraph = function(data, opts) {
      var paper = this;
      if (!opts)
        opts = {};
      var bubbles = new Bubbles(data, paper, opts);
      return bubbles;
  }; 
  
  String.prototype.bubbles_truncate = function(length){
    text = this;
    if(text.length > length){
      text = text.substring(0, length) + '...';
    }
    return text;
  };
  Array.prototype.bubbles_sort_by = function(sort_val, direction) {
    if(direction == 'DESC'){
      return this.sort(function(a,b){return b[sort_val] - a[sort_val];});
    }
    else{ //ASC by default
      return this.sort(function(a,b){return a[sort_val] - b[sort_val];});
    }
  };

  Array.prototype.bubbles_min_max = function(val) {
    this.bubbles_sort_by(val);
    return {min: this[0][val], max: this[this.length-1][val]};
  };
  
  Array.prototype.bubbles_map = function(val){
    var array = [];
    $.each(this, function(i,e){
      array.push(e[val]);
    });
    return array;
  };
  
  Array.prototype.bubbles_map_and_sum = function(val) {
    var sum = 0;
    $.each(this, function(index, e){
      sum += (typeof e[val] == 'number') ? e[val] : 0;
    });
    return sum;
  };
  
  Array.prototype.bubbles_delete = function(element){
    var array = this;
    $.each(array, function(i, e){
      if(e===element){
        array.splice(array.index(e),1);
      }
    });
    return array;
  };
  
  Array.prototype.index = function(object){
    if(this.length > 0){
      for(var i=0; i<this.length; i++){
        if(this[i]==object){
      	  return i;
      	}
      }
      return -1;
    }
    else{
      return -1;
    }
  };
