File: lib/SVG/Graph/Plot.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: SVG#3
  module: Graph#4
  class: Plot#88
inherits from
  Graph ( SVG::Graph )
has properties
method: set_defaults #95
attribute: scale_x_divisions [RW] #111
attribute: scale_y_divisions [RW] #118
attribute: scale_x_integers [RW] #120
attribute: scale_y_integers [RW] #122
attribute: area_fill [RW] #124
attribute: show_data_points [RW] #127
attribute: min_x_value [RW] #129
attribute: min_y_value [RW] #131
method: add_data #137
method: keys #160
method: calculate_left_margin #164
method: calculate_right_margin #170
constant: X #177
constant: Y #178
method: x_range #179
method: get_x_values #197
alias: get_x_labels get_x_values #203
method: field_width #205
method: y_range #214
method: get_y_values #232
alias: get_y_labels get_y_values #238
method: field_height #240
method: draw_data #252
method: format #306
method: get_css #310

Class Hierarchy

Object ( Builtin-Module )
Graph ( SVG::Graph )
  Plot    #88

Code

   1  require 'SVG/Graph/Graph'
   2 
   3  module SVG
   4    module Graph
   5      # === For creating SVG plots of scalar data
   6      # 
   7      # = Synopsis
   8      # 
   9      #   require 'SVG/Graph/Plot'
  10      # 
  11      #   # Data sets are x,y pairs
  12      #   # Note that multiple data sets can differ in length, and that the
  13      #   # data in the datasets needn't be in order; they will be ordered
  14      #   # by the plot along the X-axis.
  15      #   projection = [
  16      #     6, 11,    0, 5,   18, 7,   1, 11,   13, 9,   1, 2,   19, 0,   3, 13,
  17      #     7, 9 
  18      #   ]
  19      #   actual = [
  20      #     0, 18,    8, 15,    9, 4,   18, 14,   10, 2,   11, 6,  14, 12,   
  21      #     15, 6,   4, 17,   2, 12
  22      #   ]
  23      #   
  24      #   graph = SVG::Graph::Plot.new({
  25      #           :height => 500,
  26      #           :width => 300,
  27      #     :key => true,
  28      #     :scale_x_integers => true,
  29      #     :scale_y_integerrs => true,
  30      #   })
  31      #   
  32      #   graph.add_data({
  33      #           :data => projection
  34      #     :title => 'Projected',
  35      #   })
  36      # 
  37      #   graph.add_data({
  38      #           :data => actual,
  39      #     :title => 'Actual',
  40      #   })
  41      #   
  42      #   print graph.burn()
  43      # 
  44      # = Description
  45      # 
  46      # Produces a graph of scalar data.
  47      # 
  48      # This object aims to allow you to easily create high quality
  49      # SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the
  50      # default style sheet or supply your own. Either way there are many options
  51      # which can be configured to give you control over how the graph is
  52      # generated - with or without a key, data elements at each point, title,
  53      # subtitle etc.
  54      #
  55      # = Examples
  56      # 
  57      # http://www.germane-software/repositories/public/SVG/test/plot.rb
  58      # 
  59      # = Notes
  60      # 
  61      # The default stylesheet handles upto 10 data sets, if you
  62      # use more you must create your own stylesheet and add the
  63      # additional settings for the extra data sets. You will know
  64      # if you go over 10 data sets as they will have no style and
  65      # be in black.
  66      #
  67      # Unlike the other types of charts, data sets must contain x,y pairs:
  68      #
  69      #   [ 1, 2 ]    # A data set with 1 point: (1,2)
  70      #   [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)  
  71      # 
  72      # = See also
  73      # 
  74      # * SVG::Graph::Graph
  75      # * SVG::Graph::BarHorizontal
  76      # * SVG::Graph::Bar
  77      # * SVG::Graph::Line
  78      # * SVG::Graph::Pie
  79      # * SVG::Graph::TimeSeries
  80      #
  81      # == Author
  82      #
  83      # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
  84      #
  85      # Copyright 2004 Sean E. Russell
  86      # This software is available under the Ruby license[LICENSE.txt]
  87      #
  88      class Plot < Graph
  89 
  90        # In addition to the defaults set by Graph::initialize, sets
  91        # [show_data_values] true
  92        # [show_data_points] true
  93        # [area_fill] false
  94        # [stacked] false
  95        def set_defaults
  96          init_with(
  97                    :show_data_values  => true,
  98                    :show_data_points  => true,
  99                    :area_fill         => false,
 100                    :stacked           => false
 101                   )
 102                   self.top_align = self.right_align = self.top_font = self.right_font = 1
 103        end
 104 
 105        # Determines the scaling for the X axis divisions.
 106        #
 107        #   graph.scale_x_divisions = 2
 108        #
 109        # would cause the graph to attempt to generate labels stepped by 2; EG:
 110        # 0,2,4,6,8...
 111        attr_accessor :scale_x_divisions
 112        # Determines the scaling for the Y axis divisions.
 113        #
 114        #   graph.scale_y_divisions = 0.5
 115        #
 116        # would cause the graph to attempt to generate labels stepped by 0.5; EG:
 117        # 0, 0.5, 1, 1.5, 2, ...
 118        attr_accessor :scale_y_divisions 
 119        # Make the X axis labels integers
 120        attr_accessor :scale_x_integers 
 121        # Make the Y axis labels integers
 122        attr_accessor :scale_y_integers 
 123        # Fill the area under the line
 124        attr_accessor :area_fill 
 125        # Show a small circle on the graph where the line
 126        # goes from one point to the next.
 127        attr_accessor :show_data_points
 128        # Set the minimum value of the X axis
 129        attr_accessor :min_x_value 
 130        # Set the minimum value of the Y axis
 131        attr_accessor :min_y_value
 132 
 133 
 134        # Adds data to the plot.  The data must be in X,Y pairs; EG
 135        #   [ 1, 2 ]    # A data set with 1 point: (1,2)
 136        #   [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)  
 137        def add_data data
 138          @data = [] unless @data
 139 
 140          raise "No data provided by #{conf.inspect}" unless data[:data] and
 141          data[:data].kind_of? Array
 142          raise "Data supplied must be x,y pairs!  "+
 143            "The data provided contained an odd set of "+
 144            "data points" unless data[:data].length % 2 == 0
 145          return if data[:data].length == 0
 146 
 147          x = []
 148          y = []
 149          data[:data].each_index {|i|
 150            (i%2 == 0 ? x : y) << data[:data][i]
 151          }
 152          sort( x, y )
 153          data[:data] = [x,y]
 154          @data << data
 155        end
 156 
 157 
 158        protected
 159 
 160        def keys
 161          @data.collect{ |x| x[:title] }
 162        end
 163 
 164        def calculate_left_margin
 165          super
 166          label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6
 167          @border_left = label_left if label_left > @border_left
 168        end
 169 
 170        def calculate_right_margin
 171          super
 172          label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6
 173          @border_right = label_right if label_right > @border_right
 174        end
 175 
 176 
 177        X = 0
 178        Y = 1
 179        def x_range
 180          max_value = @data.collect{|x| x[:data][X][-1] }.max
 181          min_value = @data.collect{|x| x[:data][X][0] }.min
 182          min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
 183 
 184          range = max_value - min_value
 185          right_pad = range == 0 ? 10 : range / 20.0
 186          scale_range = (max_value + right_pad) - min_value
 187 
 188          scale_division = scale_x_divisions || (scale_range / 10.0)
 189 
 190          if scale_x_integers
 191            scale_division = scale_division < 1 ? 1 : scale_division.round
 192          end
 193 
 194          [min_value, max_value, scale_division]
 195        end
 196 
 197        def get_x_values
 198          min_value, max_value, scale_division = x_range
 199          rv = []
 200          min_value.step( max_value, scale_division ) {|v| rv << v}
 201          return rv
 202        end
 203        alias :get_x_labels :get_x_values
 204 
 205        def field_width
 206          values = get_x_values
 207          max = @data.collect{|x| x[:data][X][-1]}.max
 208          dx = (max - values[-1]).to_f / (values[-1] - values[-2])
 209          (@graph_width.to_f - font_size*2*right_font) /
 210            (values.length + dx - right_align)
 211        end
 212 
 213 
 214        def y_range
 215          max_value = @data.collect{|x| x[:data][Y].max }.max
 216          min_value = @data.collect{|x| x[:data][Y].min }.min
 217          min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value
 218 
 219          range = max_value - min_value
 220          top_pad = range == 0 ? 10 : range / 20.0
 221          scale_range = (max_value + top_pad) - min_value
 222 
 223          scale_division = scale_y_divisions || (scale_range / 10.0)
 224 
 225          if scale_y_integers
 226            scale_division = scale_division < 1 ? 1 : scale_division.round
 227          end
 228 
 229          return [min_value, max_value, scale_division]
 230        end
 231 
 232        def get_y_values
 233          min_value, max_value, scale_division = y_range
 234          rv = []
 235          min_value.step( max_value, scale_division ) {|v| rv << v}
 236          return rv
 237        end
 238        alias :get_y_labels :get_y_values
 239 
 240        def field_height
 241          values = get_y_values
 242          max = @data.collect{|x| x[:data][Y].max }.max
 243          if values.length == 1
 244            dx = values[-1]
 245          else
 246            dx = (max - values[-1]).to_f / (values[-1] - values[-2])
 247          end
 248          (@graph_height.to_f - font_size*2*top_font) /
 249            (values.length + dx - top_align)
 250        end
 251 
 252        def draw_data
 253          line = 1
 254 
 255          x_min, x_max, x_div = x_range
 256          y_min, y_max, y_div = y_range
 257          x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min)
 258          y_step = (@graph_height.to_f -  font_size*2) / (y_max-y_min)
 259 
 260          for data in @data
 261            x_points = data[:data][X]
 262            y_points = data[:data][Y]
 263 
 264            lpath = "L"
 265            x_start = 0
 266            y_start = 0
 267            x_points.each_index { |idx|
 268              x = (x_points[idx] -  x_min) * x_step
 269              y = @graph_height - (y_points[idx] -  y_min) * y_step
 270              x_start, y_start = x,y if idx == 0
 271              lpath << "#{x} #{y} "
 272            }
 273 
 274            if area_fill
 275              @graph.add_element( "path", {
 276                "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z",
 277                "class" => "fill#{line}"
 278              })
 279            end
 280 
 281            @graph.add_element( "path", {
 282              "d" => "M#{x_start} #{y_start} #{lpath}",
 283              "class" => "line#{line}"
 284            })
 285 
 286            if show_data_points || show_data_values
 287              x_points.each_index { |idx|
 288                x = (x_points[idx] -  x_min) * x_step
 289                y = @graph_height - (y_points[idx] -  y_min) * y_step
 290                if show_data_points
 291                  @graph.add_element( "circle", {
 292                    "cx" => x.to_s,
 293                    "cy" => y.to_s,
 294                    "r" => "2.5",
 295                    "class" => "dataPoint#{line}"
 296                  })
 297                  add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups
 298                end
 299                make_datapoint_text( x, y-6, y_points[idx] ) if show_data_values
 300              }
 301            end
 302            line += 1
 303          end
 304        end
 305 
 306        def format x, y
 307          "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})"
 308        end
 309        
 310        def get_css
 311          return <<EOL
 312  /* default line styles */
 313  .line1{
 314          fill: none;
 315          stroke: #ff0000;
 316          stroke-width: 1px;
 317  }
 318  .line2{
 319          fill: none;
 320          stroke: #0000ff;
 321          stroke-width: 1px;
 322  }
 323  .line3{
 324          fill: none;
 325          stroke: #00ff00;
 326          stroke-width: 1px;
 327  }
 328  .line4{
 329          fill: none;
 330          stroke: #ffcc00;
 331          stroke-width: 1px;
 332  }
 333  .line5{
 334          fill: none;
 335          stroke: #00ccff;
 336          stroke-width: 1px;
 337  }
 338  .line6{
 339          fill: none;
 340          stroke: #ff00ff;
 341          stroke-width: 1px;
 342  }
 343  .line7{
 344          fill: none;
 345          stroke: #00ffff;
 346          stroke-width: 1px;
 347  }
 348  .line8{
 349          fill: none;
 350          stroke: #ffff00;
 351          stroke-width: 1px;
 352  }
 353  .line9{
 354          fill: none;
 355          stroke: #ccc6666;
 356          stroke-width: 1px;
 357  }
 358  .line10{
 359          fill: none;
 360          stroke: #663399;
 361          stroke-width: 1px;
 362  }
 363  .line11{
 364          fill: none;
 365          stroke: #339900;
 366          stroke-width: 1px;
 367  }
 368  .line12{
 369          fill: none;
 370          stroke: #9966FF;
 371          stroke-width: 1px;
 372  }
 373  /* default fill styles */
 374  .fill1{
 375          fill: #cc0000;
 376          fill-opacity: 0.2;
 377          stroke: none;
 378  }
 379  .fill2{
 380          fill: #0000cc;
 381          fill-opacity: 0.2;
 382          stroke: none;
 383  }
 384  .fill3{
 385          fill: #00cc00;
 386          fill-opacity: 0.2;
 387          stroke: none;
 388  }
 389  .fill4{
 390          fill: #ffcc00;
 391          fill-opacity: 0.2;
 392          stroke: none;
 393  }
 394  .fill5{
 395          fill: #00ccff;
 396          fill-opacity: 0.2;
 397          stroke: none;
 398  }
 399  .fill6{
 400          fill: #ff00ff;
 401          fill-opacity: 0.2;
 402          stroke: none;
 403  }
 404  .fill7{
 405          fill: #00ffff;
 406          fill-opacity: 0.2;
 407          stroke: none;
 408  }
 409  .fill8{
 410          fill: #ffff00;
 411          fill-opacity: 0.2;
 412          stroke: none;
 413  }
 414  .fill9{
 415          fill: #cc6666;
 416          fill-opacity: 0.2;
 417          stroke: none;
 418  }
 419  .fill10{
 420          fill: #663399;
 421          fill-opacity: 0.2;
 422          stroke: none;
 423  }
 424  .fill11{
 425          fill: #339900;
 426          fill-opacity: 0.2;
 427          stroke: none;
 428  }
 429  .fill12{
 430          fill: #9966FF;
 431          fill-opacity: 0.2;
 432          stroke: none;
 433  }
 434  /* default line styles */
 435  .key1,.dataPoint1{
 436          fill: #ff0000;
 437          stroke: none;
 438          stroke-width: 1px;
 439  }
 440  .key2,.dataPoint2{
 441          fill: #0000ff;
 442          stroke: none;
 443          stroke-width: 1px;
 444  }
 445  .key3,.dataPoint3{
 446          fill: #00ff00;
 447          stroke: none;
 448          stroke-width: 1px;
 449  }
 450  .key4,.dataPoint4{
 451          fill: #ffcc00;
 452          stroke: none;
 453          stroke-width: 1px;
 454  }
 455  .key5,.dataPoint5{
 456          fill: #00ccff;
 457          stroke: none;
 458          stroke-width: 1px;
 459  }
 460  .key6,.dataPoint6{
 461          fill: #ff00ff;
 462          stroke: none;
 463          stroke-width: 1px;
 464  }
 465  .key7,.dataPoint7{
 466          fill: #00ffff;
 467          stroke: none;
 468          stroke-width: 1px;
 469  }
 470  .key8,.dataPoint8{
 471          fill: #ffff00;
 472          stroke: none;
 473          stroke-width: 1px;
 474  }
 475  .key9,.dataPoint9{
 476          fill: #cc6666;
 477          stroke: none;
 478          stroke-width: 1px;
 479  }
 480  .key10,.dataPoint10{
 481          fill: #663399;
 482          stroke: none;
 483          stroke-width: 1px;
 484  }
 485  .key11,.dataPoint11{
 486          fill: #339900;
 487          stroke: none;
 488          stroke-width: 1px;
 489  }
 490  .key12,.dataPoint12{
 491          fill: #9966FF;
 492          stroke: none;
 493          stroke-width: 1px;
 494  }
 495  EOL
 496        end
 497 
 498      end
 499    end
 500  end