File: lib/SVG/Graph/Schedule.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: SVG#4
  module: Graph#5
  class: Schedule#84
inherits from
  Graph ( SVG::Graph )
has properties
method: set_defaults #89
attribute: x_label_format [RW] #101
attribute: timescale_divisions [RW] #112
attribute: popup_format [RW] #114
attribute: min_x_value [RW] #115
attribute: scale_x_divisions [RW] #116
attribute: scale_x_integers [RW] #117
attribute: bar_gap [RW] #118
method: add_data #143
method: min_x_value= / 1 #174
method: format #180
method: get_x_labels #184
method: y_label_offset / 1 #188
method: get_y_labels #192
method: draw_data #196
method: get_css #225
method: x_range #304
method: get_x_values #322

Class Hierarchy

Object ( Builtin-Module )
Graph ( SVG::Graph )
  Schedule    #84

Code

   1  require 'SVG/Graph/Plot'
   2  require 'parsedate'
   3 
   4  module SVG
   5    module Graph
   6      # === For creating SVG plots of scalar temporal data
   7      # 
   8      # = Synopsis
   9      # 
  10      #   require 'SVG/Graph/Schedule'
  11      # 
  12      #   # Data sets are label, start, end tripples.
  13      #   data1 = [
  14      #     "Housesitting", "6/17/04", "6/19/04", 
  15      #     "Summer Session", "6/15/04", "8/15/04",
  16      #   ]
  17      #
  18      #   graph = SVG::Graph::Schedule.new( {
  19      #     :width => 640,
  20      #     :height => 480,
  21      #     :graph_title => title,
  22      #     :show_graph_title => true,
  23      #     :no_css => true,
  24      #     :scale_x_integers => true,
  25      #     :scale_y_integers => true,
  26      #     :min_x_value => 0,
  27      #     :min_y_value => 0,
  28      #     :show_data_labels => true,
  29      #     :show_x_guidelines => true,
  30      #     :show_x_title => true,
  31      #     :x_title => "Time",
  32      #     :stagger_x_labels => true,
  33      #     :stagger_y_labels => true,
  34      #     :x_label_format => "%m/%d/%y",
  35      #   })
  36      #   
  37      #   graph.add_data({
  38      #           :data => data1,
  39      #     :title => 'Data',
  40      #   })
  41      # 
  42      #   print graph.burn()
  43      #
  44      # = Description
  45      # 
  46      # Produces a graph of temporal scalar data.
  47      # 
  48      # = Examples
  49      #
  50      # http://www.germane-software/repositories/public/SVG/test/schedule.rb
  51      # 
  52      # = Notes
  53      # 
  54      # The default stylesheet handles upto 10 data sets, if you
  55      # use more you must create your own stylesheet and add the
  56      # additional settings for the extra data sets. You will know
  57      # if you go over 10 data sets as they will have no style and
  58      # be in black.
  59      #
  60      # Note that multiple data sets within the same chart can differ in 
  61      # length, and that the data in the datasets needn't be in order; 
  62      # they will be ordered by the plot along the X-axis.
  63      # 
  64      # The dates must be parseable by ParseDate, but otherwise can be
  65      # any order of magnitude (seconds within the hour, or years)
  66      # 
  67      # = See also
  68      # 
  69      # * SVG::Graph::Graph
  70      # * SVG::Graph::BarHorizontal
  71      # * SVG::Graph::Bar
  72      # * SVG::Graph::Line
  73      # * SVG::Graph::Pie
  74      # * SVG::Graph::Plot
  75      # * SVG::Graph::TimeSeries
  76      #
  77      # == Author
  78      #
  79      # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
  80      #
  81      # Copyright 2004 Sean E. Russell
  82      # This software is available under the Ruby license[LICENSE.txt]
  83      #
  84      class Schedule < Graph
  85        # In addition to the defaults set by Graph::initialize and
  86        # Plot::set_defaults, sets:
  87        # [x_label_format] '%Y-%m-%d %H:%M:%S'
  88        # [popup_format]  '%Y-%m-%d %H:%M:%S'
  89        def set_defaults
  90          init_with(
  91            :x_label_format     => '%Y-%m-%d %H:%M:%S',
  92            :popup_format       => '%Y-%m-%d %H:%M:%S',
  93            :scale_x_divisions  => false,
  94            :scale_x_integers   => false,
  95            :bar_gap            => true
  96          )
  97        end
  98 
  99        # The format string use do format the X axis labels.
 100        # See Time::strformat
 101        attr_accessor :x_label_format
 102        # Use this to set the spacing between dates on the axis.  The value
 103        # must be of the form 
 104        # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?"
 105        # 
 106        # EG:
 107        #
 108        #   graph.timescale_divisions = "2 weeks"
 109        #
 110        # will cause the chart to try to divide the X axis up into segments of
 111        # two week periods.
 112        attr_accessor :timescale_divisions
 113        # The formatting used for the popups.  See x_label_format
 114        attr_accessor :popup_format
 115        attr_accessor :min_x_value
 116        attr_accessor :scale_x_divisions
 117        attr_accessor :scale_x_integers
 118        attr_accessor :bar_gap
 119 
 120        # Add data to the plot.
 121        #
 122        #   # A data set with 1 point: Lunch from 12:30 to 14:00
 123        #   d1 = [ "Lunch", "12:30", "14:00" ] 
 124        #   # A data set with 2 points: "Cats" runs from 5/11/03 to 7/15/04, and
 125        #   #                           "Henry V" runs from 6/12/03 to 8/20/03
 126        #   d2 = [ "Cats", "5/11/03", "7/15/04",
 127        #          "Henry V", "6/12/03", "8/20/03" ]
 128        #                                
 129        #   graph.add_data( 
 130        #     :data => d1,
 131        #     :title => 'Meetings'
 132        #   )
 133        #   graph.add_data(
 134        #     :data => d2,
 135        #     :title => 'Plays'
 136        #   )
 137        #
 138        # Note that the data must be in time,value pairs, and that the date format
 139        # may be any date that is parseable by ParseDate.
 140        # Also note that, in this example, we're mixing scales; the data from d1
 141        # will probably not be discernable if both data sets are plotted on the same
 142        # graph, since d1 is too granular.
 143        def add_data data
 144          @data = [] unless @data
 145         
 146          raise "No data provided by #{conf.inspect}" unless data[:data] and
 147                                                      data[:data].kind_of? Array
 148          raise "Data supplied must be title,from,to tripples!  "+
 149            "The data provided contained an odd set of "+
 150            "data points" unless data[:data].length % 3 == 0
 151          return if data[:data].length == 0
 152 
 153 
 154          y = []
 155          x_start = []
 156          x_end = []
 157          data[:data].each_index {|i|
 158            im3 = i%3
 159            if im3 == 0
 160              y << data[:data][i]
 161            else
 162              arr = ParseDate.parsedate( data[:data][i] )
 163              t = Time.local( *arr[0,6].compact )
 164              (im3 == 1 ? x_start : x_end) << t.to_i
 165            end
 166          }
 167          sort( x_start, x_end, y )
 168          @data = [x_start, x_end, y ]
 169        end
 170 
 171 
 172        protected
 173 
 174        def min_x_value=(value)
 175          arr = ParseDate.parsedate( value )
 176          @min_x_value = Time.local( *arr[0,6].compact ).to_i
 177        end
 178 
 179 
 180        def format x, y
 181          Time.at( x ).strftime( popup_format )
 182        end
 183 
 184        def get_x_labels
 185          rv = get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) }
 186        end
 187 
 188        def y_label_offset( height )
 189          height / -2.0
 190        end
 191 
 192        def get_y_labels
 193          @data[2]
 194        end
 195 
 196        def draw_data
 197          fieldheight = field_height
 198          fieldwidth = field_width
 199 
 200          bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
 201          subbar_height = fieldheight - bargap
 202          
 203          field_count = 1
 204          y_mod = (subbar_height / 2) + (font_size / 2)
 205          min,max,div = x_range
 206          scale = (@graph_width.to_f - font_size*2) / (max-min)
 207          @data[0].each_index { |i|
 208            x_start = @data[0][i]
 209            x_end = @data[1][i]
 210            y = @graph_height - (fieldheight * field_count)
 211            bar_width = (x_end-x_start) * scale
 212            bar_start = x_start * scale - (min * scale)
 213          
 214            @graph.add_element( "rect", {
 215              "x" => bar_start.to_s,
 216              "y" => y.to_s,
 217              "width" => bar_width.to_s,
 218              "height" => subbar_height.to_s,
 219              "class" => "fill#{field_count+1}"
 220            })
 221            field_count += 1
 222          }
 223        end
 224 
 225        def get_css
 226          return <<EOL
 227  /* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
 228  .key1,.fill1{
 229          fill: #ff0000;
 230          fill-opacity: 0.5;
 231          stroke: none;
 232          stroke-width: 0.5px;
 233  }
 234  .key2,.fill2{
 235          fill: #0000ff;
 236          fill-opacity: 0.5;
 237          stroke: none;
 238          stroke-width: 1px;
 239  }
 240  .key3,.fill3{
 241          fill: #00ff00;
 242          fill-opacity: 0.5;
 243          stroke: none;
 244          stroke-width: 1px;
 245  }
 246  .key4,.fill4{
 247          fill: #ffcc00;
 248          fill-opacity: 0.5;
 249          stroke: none;
 250          stroke-width: 1px;
 251  }
 252  .key5,.fill5{
 253          fill: #00ccff;
 254          fill-opacity: 0.5;
 255          stroke: none;
 256          stroke-width: 1px;
 257  }
 258  .key6,.fill6{
 259          fill: #ff00ff;
 260          fill-opacity: 0.5;
 261          stroke: none;
 262          stroke-width: 1px;
 263  }
 264  .key7,.fill7{
 265          fill: #00ffff;
 266          fill-opacity: 0.5;
 267          stroke: none;
 268          stroke-width: 1px;
 269  }
 270  .key8,.fill8{
 271          fill: #ffff00;
 272          fill-opacity: 0.5;
 273          stroke: none;
 274          stroke-width: 1px;
 275  }
 276  .key9,.fill9{
 277          fill: #cc6666;
 278          fill-opacity: 0.5;
 279          stroke: none;
 280          stroke-width: 1px;
 281  }
 282  .key10,.fill10{
 283          fill: #663399;
 284          fill-opacity: 0.5;
 285          stroke: none;
 286          stroke-width: 1px;
 287  }
 288  .key11,.fill11{
 289          fill: #339900;
 290          fill-opacity: 0.5;
 291          stroke: none;
 292          stroke-width: 1px;
 293  }
 294  .key12,.fill12{
 295          fill: #9966FF;
 296          fill-opacity: 0.5;
 297          stroke: none;
 298          stroke-width: 1px;
 299  }
 300  EOL
 301        end
 302        
 303        private
 304        def x_range
 305          max_value = [ @data[0][-1], @data[1].max ].max 
 306          min_value = [ @data[0][0], @data[1].min ].min
 307          min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
 308 
 309          range = max_value - min_value
 310          right_pad = range == 0 ? 10 : range / 20.0
 311          scale_range = (max_value + right_pad) - min_value
 312 
 313          scale_division = scale_x_divisions || (scale_range / 10.0)
 314 
 315          if scale_x_integers
 316            scale_division = scale_division < 1 ? 1 : scale_division.round
 317          end
 318 
 319          [min_value, max_value, scale_division]
 320        end
 321 
 322        def get_x_values
 323          rv = []
 324          min, max, scale_division = x_range
 325          if timescale_divisions
 326            timescale_divisions =~ /(\d+) ?(days|weeks|months|years|hours|minutes|seconds)?/
 327            division_units = $2 ? $2 : "days"
 328            amount = $1.to_i
 329            if amount
 330              step =  nil
 331              case division_units
 332              when "months"
 333                cur = min
 334                while cur < max
 335                  rv << cur
 336                  arr = Time.at( cur ).to_a
 337                  arr[4] += amount
 338                  if arr[4] > 12
 339                    arr[5] += (arr[4] / 12).to_i
 340                    arr[4] = (arr[4] % 12)
 341                  end
 342                  cur = Time.local(*arr).to_i
 343                end
 344              when "years"
 345                cur = min
 346                while cur < max
 347                  rv << cur
 348                  arr = Time.at( cur ).to_a
 349                  arr[5] += amount
 350                  cur = Time.local(*arr).to_i
 351                end
 352              when "weeks"
 353                step = 7 * 24 * 60 * 60 * amount
 354              when "days"
 355                step = 24 * 60 * 60 * amount
 356              when "hours"
 357                step = 60 * 60 * amount
 358              when "minutes"
 359                step = 60 * amount
 360              when "seconds"
 361                step = amount
 362              end
 363              min.step( max, step ) {|v| rv << v} if step
 364 
 365              return rv
 366            end
 367          end
 368          min.step( max, scale_division ) {|v| rv << v}
 369          return rv
 370        end
 371      end
 372    end
 373  end