File: lib/SVG/Graph/Line.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: SVG#3
  module: Graph#4
  class: Line#71
inherits from
  Graph ( SVG::Graph )
has properties
attribute: show_data_points [RW] #74
attribute: stacked [RW] #77
attribute: area_fill [RW] #79
method: initialize #85
method: set_defaults #97
method: max_value #110
method: min_value #130
method: get_x_labels #144
method: calculate_left_margin #148
method: get_y_labels #154
method: calc_coords / 4 #174
method: draw_data #182
method: get_css #255

Class Hierarchy

Object ( Builtin-Module )
Graph ( SVG::Graph )
  Line    #71

Code

   1  require 'SVG/Graph/Graph'
   2 
   3  module SVG
   4    module Graph
   5      # === Create presentation quality SVG line graphs easily
   6      # 
   7      # = Synopsis
   8      # 
   9      #   require 'SVG/Graph/Line'
  10      # 
  11      #   fields = %w(Jan Feb Mar);
  12      #   data_sales_02 = [12, 45, 21]
  13      #   data_sales_03 = [15, 30, 40]
  14      #   
  15      #   graph = SVG::Graph::Line.new({
  16      #           :height => 500,
  17      #           :width => 300,
  18      #     :fields => fields,
  19      #   })
  20      #   
  21      #   graph.add_data({
  22      #           :data => data_sales_02,
  23      #     :title => 'Sales 2002',
  24      #   })
  25      # 
  26      #   graph.add_data({
  27      #           :data => data_sales_03,
  28      #     :title => 'Sales 2003',
  29      #   })
  30      #   
  31      #   print "Content-type: image/svg+xml\r\n\r\n";
  32      #   print graph.burn();
  33      # 
  34      # = Description
  35      # 
  36      # This object aims to allow you to easily create high quality
  37      # SVG line graphs. You can either use the default style sheet
  38      # or supply your own. Either way there are many options which can
  39      # be configured to give you control over how the graph is
  40      # generated - with or without a key, data elements at each point,
  41      # title, subtitle etc.
  42      # 
  43      # = Examples
  44      # 
  45      # http://www.germane-software/repositories/public/SVG/test/single.rb
  46      # 
  47      # = Notes
  48      # 
  49      # The default stylesheet handles upto 10 data sets, if you
  50      # use more you must create your own stylesheet and add the
  51      # additional settings for the extra data sets. You will know
  52      # if you go over 10 data sets as they will have no style and
  53      # be in black.
  54      # 
  55      # = See also
  56      # 
  57      # * SVG::Graph::Graph
  58      # * SVG::Graph::BarHorizontal
  59      # * SVG::Graph::Bar
  60      # * SVG::Graph::Pie
  61      # * SVG::Graph::Plot
  62      # * SVG::Graph::TimeSeries
  63      #
  64      # == Author
  65      #
  66      # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
  67      #
  68      # Copyright 2004 Sean E. Russell
  69      # This software is available under the Ruby license[LICENSE.txt]
  70      #
  71      class Line < SVG::Graph::Graph
  72        #    Show a small circle on the graph where the line
  73        #    goes from one point to the next.
  74        attr_accessor :show_data_points
  75        #    Accumulates each data set. (i.e. Each point increased by sum of 
  76        #   all previous series at same point). Default is 0, set to '1' to show.
  77        attr_accessor :stacked
  78        # Fill in the area under the plot if true
  79        attr_accessor :area_fill
  80 
  81        # The constructor takes a hash reference, fields (the names for each
  82        # field on the X axis) MUST be set, all other values are defaulted to 
  83        # those shown above - with the exception of style_sheet which defaults
  84        # to using the internal style sheet.
  85        def initialize config
  86          raise "fields was not supplied or is empty" unless config[:fields] &&
  87          config[:fields].kind_of?(Array) &&
  88          config[:fields].length > 0
  89                                  super
  90                          end
  91 
  92        # In addition to the defaults set in Graph::initialize, sets
  93        # [show_data_points] true
  94        # [show_data_values] true
  95        # [stacked] false
  96        # [area_fill] false
  97                          def set_defaults
  98          init_with(
  99            :show_data_points   => true,
 100            :show_data_values   => true,
 101            :stacked            => false,
 102            :area_fill          => false
 103          )
 104 
 105          self.top_align = self.top_font = self.right_align = self.right_font = 1
 106        end
 107 
 108        protected
 109 
 110        def max_value
 111          max = 0
 112          
 113          if (stacked == true) then
 114            sums = Array.new(@config[:fields].length).fill(0)
 115 
 116            @data.each do |data|
 117              sums.each_index do |i|
 118                sums[i] += data[:data][i].to_f
 119              end
 120            end
 121            
 122            max = sums.max
 123          else
 124            max = @data.collect{|x| x[:data].max}.max
 125          end
 126 
 127          return max
 128        end
 129 
 130        def min_value
 131          min = 0
 132          
 133          if (min_scale_value.nil? == false) then
 134            min = min_scale_value
 135          elsif (stacked == true) then
 136            min = @data[-1][:data].min
 137          else
 138            min = @data.collect{|x| x[:data].min}.min
 139          end
 140 
 141          return min
 142        end
 143 
 144        def get_x_labels
 145          @config[:fields]
 146        end
 147 
 148        def calculate_left_margin
 149          super
 150          label_left = @config[:fields][0].length / 2 * font_size * 0.6
 151          @border_left = label_left if label_left > @border_left
 152        end
 153 
 154        def get_y_labels
 155          maxvalue = max_value
 156          minvalue = min_value
 157          range = maxvalue - minvalue
 158          top_pad = range == 0 ? 10 : range / 20.0
 159          scale_range = (maxvalue + top_pad) - minvalue
 160 
 161          scale_division = scale_divisions || (scale_range / 10.0)
 162 
 163          if scale_integers
 164            scale_division = scale_division < 1 ? 1 : scale_division.round
 165          end
 166 
 167          rv = []
 168          maxvalue = maxvalue%scale_division == 0 ? 
 169            maxvalue : maxvalue + scale_division
 170          minvalue.step( maxvalue, scale_division ) {|v| rv << v}
 171          return rv
 172        end
 173 
 174        def calc_coords(field, value, width = field_width, height = field_height)
 175          coords = {:x => 0, :y => 0}
 176          coords[:x] = width * field
 177          coords[:y] = @graph_height - value * height
 178        
 179          return coords
 180        end
 181 
 182        def draw_data
 183          minvalue = min_value
 184          fieldheight = (@graph_height.to_f - font_size*2*top_font) / 
 185                           (get_y_labels.max - get_y_labels.min)
 186          fieldwidth = field_width
 187          line = @data.length
 188 
 189          prev_sum = Array.new(@config[:fields].length).fill(0)
 190          cum_sum = Array.new(@config[:fields].length).fill(-minvalue)
 191 
 192          for data in @data.reverse
 193            lpath = ""
 194            apath = ""
 195 
 196            if not stacked then cum_sum.fill(-minvalue) end
 197            
 198            data[:data].each_index do |i|
 199              cum_sum[i] += data[:data][i]
 200              
 201              c = calc_coords(i, cum_sum[i], fieldwidth, fieldheight)
 202              
 203              lpath << "#{c[:x]} #{c[:y]} "
 204            end
 205          
 206            if area_fill
 207              if stacked then
 208                (prev_sum.length - 1).downto 0 do |i|
 209                  c = calc_coords(i, prev_sum[i], fieldwidth, fieldheight)
 210                  
 211                  apath << "#{c[:x]} #{c[:y]} "
 212                end
 213            
 214                c = calc_coords(0, prev_sum[0], fieldwidth, fieldheight)
 215              else
 216                apath = "V#@graph_height"
 217                c = calc_coords(0, 0, fieldwidth, fieldheight)
 218              end
 219                
 220              @graph.add_element("path", {
 221                "d" => "M#{c[:x]} #{c[:y]} L" + lpath + apath + "Z",
 222                "class" => "fill#{line}"
 223              })
 224            end
 225          
 226            @graph.add_element("path", {
 227              "d" => "M0 #@graph_height L" + lpath,
 228              "class" => "line#{line}"
 229            })
 230            
 231            if show_data_points || show_data_values
 232              cum_sum.each_index do |i|
 233                if show_data_points
 234                  @graph.add_element( "circle", {
 235                    "cx" => (fieldwidth * i).to_s,
 236                    "cy" => (@graph_height - cum_sum[i] * fieldheight).to_s,
 237                    "r" => "2.5",
 238                    "class" => "dataPoint#{line}"
 239                  })
 240                end
 241                make_datapoint_text( 
 242                  fieldwidth * i, 
 243                  @graph_height - cum_sum[i] * fieldheight - 6,
 244                  cum_sum[i] + minvalue
 245                )
 246              end
 247            end
 248 
 249            prev_sum = cum_sum.dup
 250            line -= 1
 251          end
 252        end
 253 
 254 
 255        def get_css
 256          return <<EOL
 257  /* default line styles */
 258  .line1{
 259          fill: none;
 260          stroke: #ff0000;
 261          stroke-width: 1px;
 262  }
 263  .line2{
 264          fill: none;
 265          stroke: #0000ff;
 266          stroke-width: 1px;
 267  }
 268  .line3{
 269          fill: none;
 270          stroke: #00ff00;
 271          stroke-width: 1px;
 272  }
 273  .line4{
 274          fill: none;
 275          stroke: #ffcc00;
 276          stroke-width: 1px;
 277  }
 278  .line5{
 279          fill: none;
 280          stroke: #00ccff;
 281          stroke-width: 1px;
 282  }
 283  .line6{
 284          fill: none;
 285          stroke: #ff00ff;
 286          stroke-width: 1px;
 287  }
 288  .line7{
 289          fill: none;
 290          stroke: #00ffff;
 291          stroke-width: 1px;
 292  }
 293  .line8{
 294          fill: none;
 295          stroke: #ffff00;
 296          stroke-width: 1px;
 297  }
 298  .line9{
 299          fill: none;
 300          stroke: #ccc6666;
 301          stroke-width: 1px;
 302  }
 303  .line10{
 304          fill: none;
 305          stroke: #663399;
 306          stroke-width: 1px;
 307  }
 308  .line11{
 309          fill: none;
 310          stroke: #339900;
 311          stroke-width: 1px;
 312  }
 313  .line12{
 314          fill: none;
 315          stroke: #9966FF;
 316          stroke-width: 1px;
 317  }
 318  /* default fill styles */
 319  .fill1{
 320          fill: #cc0000;
 321          fill-opacity: 0.2;
 322          stroke: none;
 323  }
 324  .fill2{
 325          fill: #0000cc;
 326          fill-opacity: 0.2;
 327          stroke: none;
 328  }
 329  .fill3{
 330          fill: #00cc00;
 331          fill-opacity: 0.2;
 332          stroke: none;
 333  }
 334  .fill4{
 335          fill: #ffcc00;
 336          fill-opacity: 0.2;
 337          stroke: none;
 338  }
 339  .fill5{
 340          fill: #00ccff;
 341          fill-opacity: 0.2;
 342          stroke: none;
 343  }
 344  .fill6{
 345          fill: #ff00ff;
 346          fill-opacity: 0.2;
 347          stroke: none;
 348  }
 349  .fill7{
 350          fill: #00ffff;
 351          fill-opacity: 0.2;
 352          stroke: none;
 353  }
 354  .fill8{
 355          fill: #ffff00;
 356          fill-opacity: 0.2;
 357          stroke: none;
 358  }
 359  .fill9{
 360          fill: #cc6666;
 361          fill-opacity: 0.2;
 362          stroke: none;
 363  }
 364  .fill10{
 365          fill: #663399;
 366          fill-opacity: 0.2;
 367          stroke: none;
 368  }
 369  .fill11{
 370          fill: #339900;
 371          fill-opacity: 0.2;
 372          stroke: none;
 373  }
 374  .fill12{
 375          fill: #9966FF;
 376          fill-opacity: 0.2;
 377          stroke: none;
 378  }
 379  /* default line styles */
 380  .key1,.dataPoint1{
 381          fill: #ff0000;
 382          stroke: none;
 383          stroke-width: 1px;
 384  }
 385  .key2,.dataPoint2{
 386          fill: #0000ff;
 387          stroke: none;
 388          stroke-width: 1px;
 389  }
 390  .key3,.dataPoint3{
 391          fill: #00ff00;
 392          stroke: none;
 393          stroke-width: 1px;
 394  }
 395  .key4,.dataPoint4{
 396          fill: #ffcc00;
 397          stroke: none;
 398          stroke-width: 1px;
 399  }
 400  .key5,.dataPoint5{
 401          fill: #00ccff;
 402          stroke: none;
 403          stroke-width: 1px;
 404  }
 405  .key6,.dataPoint6{
 406          fill: #ff00ff;
 407          stroke: none;
 408          stroke-width: 1px;
 409  }
 410  .key7,.dataPoint7{
 411          fill: #00ffff;
 412          stroke: none;
 413          stroke-width: 1px;
 414  }
 415  .key8,.dataPoint8{
 416          fill: #ffff00;
 417          stroke: none;
 418          stroke-width: 1px;
 419  }
 420  .key9,.dataPoint9{
 421          fill: #cc6666;
 422          stroke: none;
 423          stroke-width: 1px;
 424  }
 425  .key10,.dataPoint10{
 426          fill: #663399;
 427          stroke: none;
 428          stroke-width: 1px;
 429  }
 430  .key11,.dataPoint11{
 431          fill: #339900;
 432          stroke: none;
 433          stroke-width: 1px;
 434  }
 435  .key12,.dataPoint12{
 436          fill: #9966FF;
 437          stroke: none;
 438          stroke-width: 1px;
 439  }
 440  EOL
 441        end
 442      end
 443    end
 444  end