SciRuby/statsample

View on GitHub
lib/statsample/graph/scatterplot.rb

Summary

Maintainability
C
1 day
Test Coverage
require 'rubyvis'
module Statsample
  module Graph
    # = Scatterplot
    # 
    # From Wikipedia:
    # A scatter plot or scattergraph is a type of mathematical diagram using
    # Cartesian coordinates to display values for two variables for a set of data.
    # 
    # The data is displayed as a collection of points, each having the value of one variable determining the position on the horizontal axis and the value of the other variable determining the position on the vertical axis.[2] This kind of plot is also called a scatter chart, scatter diagram and scatter graph.
    # == Usage
    # === Svg output
    #  a = Daru::Vector.new([1,2,3,4])
    #  b = Daru::Vector.new([3,4,5,6])
    #  puts Statsample::Graph::Scatterplot.new(a,b).to_svg
    # === Using ReportBuilder
    #  a = Daru::Vector.new([1,2,3,4])
    #  b = Daru::Vector.new([3,4,5,6])
    #  rb=ReportBuilder.new
    #  rb.add(Statsample::Graph::Scatterplot.new(a,b))
    #  rb.save_html('scatter.html')
    
    class Scatterplot
      include Summarizable
      attr_accessor :name
      # Total width of Scatterplot
      attr_accessor :width
      # Total height of Scatterplot
      attr_accessor :height
      attr_accessor :dot_alpha
      # Add a line on median of x and y axis 
      attr_accessor :line_median
      # Top margin
      attr_accessor :margin_top
      # Bottom margin
      attr_accessor :margin_bottom
      # Left margin
      attr_accessor :margin_left
      # Right margin
      attr_accessor :margin_right
      
      attr_reader   :data
      attr_reader :v1,:v2
      
      # Array with assignation to groups of bars
      # For example, for four vectors, 
      #   boxplot.groups=[1,2,1,3]
      # Assign same color to first and third element, and different to
      # second and fourth
      attr_accessor :groups

      
      attr_reader :x_scale, :y_scale
      # Minimum value on x axis. Calculated automaticly from data if not set
      attr_accessor :minimum_x
      # Maximum value on x axis. Calculated automaticly from data if not set
      attr_accessor :maximum_x
      # Minimum value on y axis. Set to 0 if not set
      attr_accessor :minimum_y
      # Maximum value on y axis. Calculated automaticly from data if not set.
      attr_accessor :maximum_y

      # Create a new Scatterplot.
      # Params:
      # * v1: Vector on X axis
      # * v2: Vector on Y axis
      # * opts: Hash of options. See attributes of Scatterplot
      def initialize(v1,v2,opts=Hash.new)
        @v1_name,@v2_name = v1.name,v2.name
        @v1,@v2           = Statsample.only_valid_clone(v1,v2)
        opts_default={
          :name=>_("Scatterplot (%s - %s)") % [@v1_name, @v2_name],
          :width=>400,
          :height=>300,
          :dot_alpha=>0.5,
          :line_median=>false,
          :margin_top=>10,
          :margin_bottom=>20,
          :margin_left=>20,
          :margin_right=>20,
          :minimum_x=>nil,
          :maximum_x=>nil,
          :minimum_y=>nil,
          :maximum_y=>nil,
          :groups=>nil
        }
        @opts=opts_default.merge(opts)
        opts_default.keys.each {|k| send("#{k}=", @opts[k]) }
        @data=[]
        @v1.each_with_index {|d1,i|
          @data.push({:x=>d1, :y=>@v2[i]})
        }
      end
      # Add a rule on median of X and Y axis
      def add_line_median(vis) # :nodoc:
        that=self
        x=@x_scale
        y=@y_scale
        vis.execute {
          rule do
            data [that.v1.median]
            left x
            stroke_style Rubyvis.color("#933").alpha(0.5)
            label(:anchor=>"top") do
              text x.tick_format
            end
          end
          rule do
            data [that.v2.median]
            bottom y
            stroke_style Rubyvis.color("#933").alpha(0.5)
            label(:anchor=>"right") do
              text y.tick_format
            end
          end  
        }
        
      end
      # Returns a Rubyvis panel with scatterplot
      def rubyvis_panel # :nodoc:
        that=self
        #p @v1.map {|v| v}
        
        @minimum_x||=@v1.min
        @maximum_x||=@v1.max
        @minimum_y||=@v2.min
        @maximum_y||=@v2.max
        
        colors=Rubyvis::Colors.category10
        
        margin_hor=margin_left + margin_right
        margin_vert=margin_top  + margin_bottom
        
        x=Rubyvis::Scale.linear(@minimum_x, @maximum_x).range(0, width - margin_hor)
        y=Rubyvis::Scale.linear(@minimum_y, @maximum_y).range(0, height - margin_vert)
        @x_scale=x
        @y_scale=y
        vis=Rubyvis::Panel.new do |pan| 
          pan.width  width  - margin_hor
          pan.height height - margin_vert
          pan.bottom margin_bottom
          pan.left   margin_left
          pan.right  margin_right
          pan.top    margin_top
          # X axis
          pan.rule do
            data y.ticks
            bottom y
            stroke_style {|d| d!=0 ? "#eee" : "#000"}
            label(:anchor=>'left') do
              visible {|d| d!=0 and  d < that.width}
              text y.tick_format
            end
          end
          
          # Y axis
          pan.rule do
            data x.ticks
            left x
            stroke_style {|d| d!=0 ? "#eee" : "#000"}
            label(:anchor=>'bottom') do
              visible {|d| d>0 and d < that.height}
              text x.tick_format
            end
          end
          # Add lines on median
          add_line_median(pan) if line_median

          pan.panel do
            data(that.data)
            dot do
              left   {|d| x[d[:x]]}
              bottom {|d| y[d[:y]]}
              
              fill_style {|v| 
                alpha=(that.dot_alpha-0.3<=0) ? 0.1 : that.dot_alpha-0.3
                if that.groups
                  
                  colors.scale(that.groups[index]).alpha(alpha)
                else
                  colors.scale(0).alpha(alpha)
                end
              }
              
              stroke_style {|v|
                if that.groups
                  colors.scale(that.groups[parent.index]).alpha(that.dot_alpha)
                else
                  colors.scale(0).alpha(that.dot_alpha)
                end
              }
              shape_radius 2
            end
          end
        end
        vis
      end

      # Returns SVG with scatterplot
      def to_svg
        rp = rubyvis_panel
        rp.render
        rp.to_svg
      end

      def report_building(builder) # :nodoc:
        builder.section(:name=>name) do |b|
          b.image(to_svg, :type=>'svg', :width=>width, :height=>height)
        end       
      end
    end
  end
end