File: lib/SVG/Graph/Graph.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: SVG#10
  module: Graph#11
has properties
constant: VERSION #12
  class: Graph#61
includes
  REXML   
inherits from
  Object ( Builtin-Module )
has properties
method: initialize / 1 #101
method: add_data #168
method: clear_data #183
method: burn #195
attribute: height [RW] #232
attribute: width [RW] #236
attribute: style_sheet [RW] #243
attribute: show_data_values [RW] #245
attribute: min_scale_value [RW] #248
attribute: show_x_labels [RW] #251
attribute: stagger_x_labels [RW] #255
attribute: stagger_y_labels [RW] #259
attribute: rotate_x_labels [RW] #262
attribute: rotate_y_labels [RW] #265
attribute: step_x_labels [RW] #271
attribute: step_include_first_x_label [RW] #276
attribute: show_y_labels [RW] #279
attribute: scale_integers [RW] #283
attribute: scale_divisions [RW] #289
attribute: show_x_title [RW] #292
attribute: x_title [RW] #294
attribute: show_y_title [RW] #297
attribute: y_title_text_direction [RW] #301
attribute: y_title [RW] #303
attribute: show_graph_title [RW] #306
attribute: graph_title [RW] #308
attribute: show_graph_subtitle [RW] #311
attribute: graph_subtitle [RW] #313
attribute: key [RW] #316
attribute: key_position [RW] #319
attribute: font_size [RW] #321
attribute: x_label_font_size [RW] #323
attribute: x_title_font_size [RW] #325
attribute: y_label_font_size [RW] #327
attribute: y_title_font_size [RW] #329
attribute: title_font_size [RW] #331
attribute: subtitle_font_size [RW] #333
attribute: key_font_size [RW] #335
attribute: show_x_guidelines [RW] #337
attribute: show_y_guidelines [RW] #339
attribute: no_css [RW] #343
attribute: add_popups [RW] #345
method: sort / 1 #350
method: init_with #356
attribute: top_align [RW] #362
attribute: top_font [RW] #362
attribute: right_align [RW] #362
attribute: right_font [RW] #362
constant: KEY_BOX_SIZE #364
method: calculate_left_margin #368
method: max_y_label_width_px #384
method: calculate_right_margin #391
method: calculate_top_margin #404
method: add_popup / 3 #413
method: calculate_bottom_margin #442
method: draw_graph #462
method: x_label_offset / 1 #495
method: make_datapoint_text / 4 #499
method: draw_x_labels #519
method: y_label_offset / 1 #570
method: field_width #575
method: field_height #581
method: draw_y_labels #588
method: draw_x_guidelines / 2 #630
method: draw_y_guidelines / 2 #641
method: draw_titles #652
method: keys #705
method: draw_legend #710
method: sort_multiple / 3 #758
method: partition / 3 #767
method: style #782
method: parse_css #794
method: add_defs #822
method: start_svg #826
method: calculate_graph_dimensions #871
method: get_style #880

Class Hierarchy

Object ( Builtin-Module )
  Graph ( SVG::Graph ) #61

Code

   1  begin
   2    require 'zlib'
   3    @@__have_zlib = true
   4  rescue
   5    @@__have_zlib = false
   6  end
   7 
   8  require 'rexml/document'
   9 
  10  module SVG
  11    module Graph
  12      VERSION = '@ANT_VERSION@'
  13 
  14      # === Base object for generating SVG Graphs
  15      # 
  16      # == Synopsis
  17      #
  18      # This class is only used as a superclass of specialized charts.  Do not
  19      # attempt to use this class directly, unless creating a new chart type.
  20      #
  21      # For examples of how to subclass this class, see the existing specific
  22      # subclasses, such as SVG::Graph::Pie.
  23      #
  24      # == Examples
  25      #
  26      # For examples of how to use this package, see either the test files, or
  27      # the documentation for the specific class you want to use.
  28      #
  29      # * file:test/plot.rb
  30      # * file:test/single.rb
  31      # * file:test/test.rb
  32      # * file:test/timeseries.rb
  33      # 
  34      # == Description
  35      # 
  36      # This package should be used as a base for creating SVG graphs.
  37      #
  38      # == Acknowledgements
  39      #
  40      # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby
  41      # port is based on.
  42      #
  43      # Stephen Morgan for creating the TT template and SVG.
  44      # 
  45      # == See
  46      #
  47      # * SVG::Graph::BarHorizontal
  48      # * SVG::Graph::Bar
  49      # * SVG::Graph::Line
  50      # * SVG::Graph::Pie
  51      # * SVG::Graph::Plot
  52      # * SVG::Graph::TimeSeries
  53      #
  54      # == Author
  55      #
  56      # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
  57      #
  58      # Copyright 2004 Sean E. Russell
  59      # This software is available under the Ruby license[LICENSE.txt]
  60      #
  61      class Graph
  62        include REXML
  63 
  64        # Initialize the graph object with the graph settings.  You won't
  65        # instantiate this class directly; see the subclass for options.
  66        # [width] 500
  67        # [height] 300
  68        # [show_x_guidelines] false
  69        # [show_y_guidelines] true
  70        # [show_data_values] true
  71        # [min_scale_value] 0
  72        # [show_x_labels] true
  73        # [stagger_x_labels] false
  74        # [rotate_x_labels] false
  75        # [step_x_labels] 1
  76        # [step_include_first_x_label] true
  77        # [show_y_labels] true
  78        # [rotate_y_labels] false
  79        # [scale_integers] false
  80        # [show_x_title] false
  81        # [x_title] 'X Field names'
  82        # [show_y_title] false
  83        # [y_title_text_direction] :bt
  84        # [y_title] 'Y Scale'
  85        # [show_graph_title] false
  86        # [graph_title] 'Graph Title'
  87        # [show_graph_subtitle] false
  88        # [graph_subtitle] 'Graph Sub Title'
  89        # [key] true,
  90        # [key_position] :right, # bottom or righ
  91        # [font_size] 12
  92        # [title_font_size] 16
  93        # [subtitle_font_size] 14
  94        # [x_label_font_size] 12
  95        # [x_title_font_size] 14
  96        # [y_label_font_size] 12
  97        # [y_title_font_size] 14
  98        # [key_font_size] 10
  99        # [no_css] false
 100        # [add_popups] false
 101        def initialize( config )
 102          @config = config
 103 
 104          self.top_align = self.top_font = self.right_align = self.right_font = 0
 105 
 106          init_with({
 107            :width                => 500,
 108            :height                => 300,
 109            :show_x_guidelines    => false,
 110            :show_y_guidelines    => true,
 111            :show_data_values     => true,
 112 
 113  #          :min_scale_value      => 0,
 114 
 115            :show_x_labels        => true,
 116            :stagger_x_labels     => false,
 117            :rotate_x_labels      => false,
 118            :step_x_labels        => 1,
 119            :step_include_first_x_label => true,
 120 
 121            :show_y_labels        => true,
 122            :rotate_y_labels      => false,
 123            :stagger_y_labels     => false,
 124            :scale_integers       => false,
 125 
 126            :show_x_title         => false,
 127            :x_title              => 'X Field names',
 128 
 129            :show_y_title         => false,
 130            :y_title_text_direction => :bt,
 131            :y_title              => 'Y Scale',
 132 
 133            :show_graph_title      => false,
 134            :graph_title          => 'Graph Title',
 135            :show_graph_subtitle  => false,
 136            :graph_subtitle        => 'Graph Sub Title',
 137            :key                  => true, 
 138            :key_position          => :right, # bottom or right
 139 
 140            :font_size            =>12,
 141            :title_font_size      =>16,
 142            :subtitle_font_size   =>14,
 143            :x_label_font_size    =>12,
 144            :x_title_font_size    =>14,
 145            :y_label_font_size    =>12,
 146            :y_title_font_size    =>14,
 147            :key_font_size        =>10,
 148            
 149            :no_css               =>false,
 150            :add_popups           =>false,
 151          })
 152 
 153                                  set_defaults if respond_to? :set_defaults
 154 
 155          init_with config
 156        end
 157 
 158        
 159        # This method allows you do add data to the graph object.
 160        # It can be called several times to add more data sets in.
 161        #
 162        #   data_sales_02 = [12, 45, 21];
 163        #   
 164        #   graph.add_data({
 165        #     :data => data_sales_02,
 166        #     :title => 'Sales 2002'
 167        #   })
 168        def add_data conf
 169          @data = [] unless defined? @data
 170 
 171          if conf[:data] and conf[:data].kind_of? Array
 172            @data << conf
 173          else
 174            raise "No data provided by #{conf.inspect}"
 175          end
 176        end
 177 
 178 
 179        # This method removes all data from the object so that you can
 180        # reuse it to create a new graph but with the same config options.
 181        #
 182        #   graph.clear_data
 183        def clear_data 
 184          @data = []
 185        end
 186 
 187 
 188        # This method processes the template with the data and
 189        # config which has been set and returns the resulting SVG.
 190        #
 191        # This method will croak unless at least one data set has
 192        # been added to the graph object.
 193        #
 194        #   print graph.burn
 195        def burn
 196          raise "No data available" unless @data.size > 0
 197          
 198          calculations if respond_to? :calculations
 199 
 200          start_svg
 201          calculate_graph_dimensions
 202          @foreground = Element.new( "g" )
 203          draw_graph
 204          draw_titles
 205          draw_legend
 206          draw_data
 207          @graph.add_element( @foreground )
 208          style
 209 
 210          data = ""
 211          @doc.write( data, 0 )
 212 
 213          if @config[:compress]
 214            if @@__have_zlib
 215              inp, out = IO.pipe
 216              gz = Zlib::GzipWriter.new( out )
 217              gz.write data
 218              gz.close
 219              data = inp.read
 220            else
 221              data << "<!-- Ruby Zlib not available for SVGZ -->";
 222            end
 223          end
 224          
 225          return data
 226        end
 227 
 228 
 229        #   Set the height of the graph box, this is the total height
 230        #   of the SVG box created - not the graph it self which auto
 231        #   scales to fix the space.
 232        attr_accessor :height
 233        #   Set the width of the graph box, this is the total width
 234        #   of the SVG box created - not the graph it self which auto
 235        #   scales to fix the space.
 236        attr_accessor :width
 237        #   Set the path to an external stylesheet, set to '' if
 238        #   you want to revert back to using the defaut internal version.
 239        #
 240        #   To create an external stylesheet create a graph using the
 241        #   default internal version and copy the stylesheet section to
 242        #   an external file and edit from there.
 243        attr_accessor :style_sheet
 244        #   (Bool) Show the value of each element of data on the graph
 245        attr_accessor :show_data_values
 246        #   The point at which the Y axis starts, defaults to '0',
 247        #   if set to nil it will default to the minimum data value.
 248        attr_accessor :min_scale_value
 249        #   Whether to show labels on the X axis or not, defaults
 250        #   to true, set to false if you want to turn them off.
 251        attr_accessor :show_x_labels
 252        #   This puts the X labels at alternative levels so if they
 253        #   are long field names they will not overlap so easily.
 254        #   Default it false, to turn on set to true.
 255        attr_accessor :stagger_x_labels
 256        #   This puts the Y labels at alternative levels so if they
 257        #   are long field names they will not overlap so easily.
 258        #   Default it false, to turn on set to true.
 259        attr_accessor :stagger_y_labels
 260        #   This turns the X axis labels by 90 degrees.
 261        #   Default it false, to turn on set to true.
 262        attr_accessor :rotate_x_labels
 263        #   This turns the Y axis labels by 90 degrees.
 264        #   Default it false, to turn on set to true.
 265        attr_accessor :rotate_y_labels
 266        #   How many "steps" to use between displayed X axis labels,
 267        #   a step of one means display every label, a step of two results
 268        #   in every other label being displayed (label <gap> label <gap> label),
 269        #   a step of three results in every third label being displayed
 270        #   (label <gap> <gap> label <gap> <gap> label) and so on.
 271        attr_accessor :step_x_labels
 272        #   Whether to (when taking "steps" between X axis labels) step from 
 273        #   the first label (i.e. always include the first label) or step from
 274        #   the X axis origin (i.e. start with a gap if step_x_labels is greater
 275        #   than one).
 276        attr_accessor :step_include_first_x_label
 277        #   Whether to show labels on the Y axis or not, defaults
 278        #   to true, set to false if you want to turn them off.
 279        attr_accessor :show_y_labels
 280        #   Ensures only whole numbers are used as the scale divisions.
 281        #   Default it false, to turn on set to true. This has no effect if 
 282        #   scale divisions are less than 1.
 283        attr_accessor :scale_integers
 284        #   This defines the gap between markers on the Y axis,
 285        #   default is a 10th of the max_value, e.g. you will have
 286        #   10 markers on the Y axis. NOTE: do not set this too
 287        #   low - you are limited to 999 markers, after that the
 288        #   graph won't generate.
 289        attr_accessor :scale_divisions
 290        #   Whether to show the title under the X axis labels,
 291        #   default is false, set to true to show.
 292        attr_accessor :show_x_title
 293        #   What the title under X axis should be, e.g. 'Months'.
 294        attr_accessor :x_title
 295        #   Whether to show the title under the Y axis labels,
 296        #   default is false, set to true to show.
 297        attr_accessor :show_y_title
 298        #   Aligns writing mode for Y axis label. 
 299        #   Defaults to :bt (Bottom to Top).
 300        #   Change to :tb (Top to Bottom) to reverse.
 301        attr_accessor :y_title_text_direction
 302        #   What the title under Y axis should be, e.g. 'Sales in thousands'.
 303        attr_accessor :y_title
 304        #   Whether to show a title on the graph, defaults
 305        #   to false, set to true to show.
 306        attr_accessor :show_graph_title
 307        #   What the title on the graph should be.
 308        attr_accessor :graph_title
 309        #   Whether to show a subtitle on the graph, defaults
 310        #   to false, set to true to show.
 311        attr_accessor :show_graph_subtitle
 312        #   What the subtitle on the graph should be.
 313        attr_accessor :graph_subtitle
 314        #   Whether to show a key, defaults to false, set to
 315        #   true if you want to show it.
 316        attr_accessor :key
 317        #   Where the key should be positioned, defaults to
 318        #   :right, set to :bottom if you want to move it.
 319        attr_accessor :key_position
 320        # Set the font size (in points) of the data point labels
 321        attr_accessor :font_size
 322        # Set the font size of the X axis labels
 323        attr_accessor :x_label_font_size
 324        # Set the font size of the X axis title
 325        attr_accessor :x_title_font_size
 326        # Set the font size of the Y axis labels
 327        attr_accessor :y_label_font_size
 328        # Set the font size of the Y axis title
 329        attr_accessor :y_title_font_size
 330        # Set the title font size
 331        attr_accessor :title_font_size
 332        # Set the subtitle font size
 333        attr_accessor :subtitle_font_size
 334        # Set the key font size
 335        attr_accessor :key_font_size
 336        # Show guidelines for the X axis
 337        attr_accessor :show_x_guidelines
 338        # Show guidelines for the Y axis
 339        attr_accessor :show_y_guidelines
 340        # Do not use CSS if set to true.  Many SVG viewers do not support CSS, but
 341        # not using CSS can result in larger SVGs as well as making it impossible to
 342        # change colors after the chart is generated.  Defaults to false.
 343        attr_accessor :no_css
 344        # Add popups for the data points on some graphs
 345        attr_accessor :add_popups
 346 
 347 
 348        protected
 349 
 350        def sort( *arrys )
 351          sort_multiple( arrys )
 352        end
 353 
 354        # Overwrite configuration options with supplied options.  Used
 355        # by subclasses.
 356        def init_with config
 357          config.each { |key, value|
 358            self.send((key.to_s+"=").to_sym, value ) if respond_to? key.to_sym
 359          }
 360        end
 361 
 362        attr_accessor :top_align, :top_font, :right_align, :right_font
 363 
 364        KEY_BOX_SIZE = 12
 365 
 366        # Override this (and call super) to change the margin to the left
 367        # of the plot area.  Results in @border_left being set.
 368        def calculate_left_margin
 369          @border_left = 7
 370          # Check for Y labels
 371          max_y_label_height_px = rotate_y_labels ? 
 372            y_label_font_size :
 373            get_y_labels.max{|a,b| 
 374              a.to_s.length<=>b.to_s.length
 375            }.to_s.length * y_label_font_size * 0.6
 376          @border_left += max_y_label_height_px if show_y_labels
 377          @border_left += max_y_label_height_px + 10 if stagger_y_labels
 378          @border_left += y_title_font_size + 5 if show_y_title
 379        end
 380 
 381 
 382        # Calculates the width of the widest Y label.  This will be the
 383        # character height if the Y labels are rotated
 384        def max_y_label_width_px
 385          return font_size if rotate_y_labels
 386        end
 387 
 388 
 389        # Override this (and call super) to change the margin to the right
 390        # of the plot area.  Results in @border_right being set.
 391        def calculate_right_margin
 392          @border_right = 7
 393          if key and key_position == :right
 394            val = keys.max { |a,b| a.length <=> b.length }
 395            @border_right += val.length * key_font_size * 0.6 
 396            @border_right += KEY_BOX_SIZE
 397            @border_right += 10    # Some padding around the box
 398          end
 399        end
 400 
 401 
 402        # Override this (and call super) to change the margin to the top
 403        # of the plot area.  Results in @border_top being set.
 404        def calculate_top_margin
 405          @border_top = 5
 406          @border_top += title_font_size if show_graph_title
 407          @border_top += 5
 408          @border_top += subtitle_font_size if show_graph_subtitle
 409        end
 410 
 411 
 412        # Adds pop-up point information to a graph.
 413        def add_popup( x, y, label )
 414          txt_width = label.length * font_size * 0.6 + 10
 415          tx = (x+txt_width > width ? x-5 : x+5)
 416          t = @foreground.add_element( "text", {
 417            "x" => tx.to_s,
 418            "y" => (y - font_size).to_s,
 419            "visibility" => "hidden",
 420          })
 421          t.attributes["style"] = "fill: #000; "+
 422            (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
 423          t.text = label.to_s
 424          t.attributes["id"] = t.object_id.to_s
 425 
 426          @foreground.add_element( "circle", {
 427            "cx" => x.to_s,
 428            "cy" => y.to_s,
 429            "r" => "10",
 430            "style" => "opacity: 0",
 431            "onmouseover" => 
 432              "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )",
 433            "onmouseout" => 
 434              "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )",
 435          })
 436 
 437        end
 438 
 439        
 440        # Override this (and call super) to change the margin to the bottom
 441        # of the plot area.  Results in @border_bottom being set.
 442        def calculate_bottom_margin
 443          @border_bottom = 7
 444          if key and key_position == :bottom
 445            @border_bottom += @data.size * (font_size + 5)
 446            @border_bottom += 10
 447          end
 448          if show_x_labels
 449                    max_x_label_height_px = (not rotate_x_labels) ? 
 450              x_label_font_size :
 451              get_x_labels.max{|a,b| 
 452                a.to_s.length<=>b.to_s.length
 453              }.to_s.length * x_label_font_size * 0.6
 454            @border_bottom += max_x_label_height_px
 455            @border_bottom += max_x_label_height_px + 10 if stagger_x_labels
 456          end
 457          @border_bottom += x_title_font_size + 5 if show_x_title
 458        end
 459 
 460 
 461        # Draws the background, axis, and labels.
 462        def draw_graph
 463          @graph = @root.add_element( "g", {
 464            "transform" => "translate( #@border_left #@border_top )"
 465          })
 466 
 467          # Background
 468          @graph.add_element( "rect", {
 469            "x" => "0",
 470            "y" => "0",
 471            "width" => @graph_width.to_s,
 472            "height" => @graph_height.to_s,
 473            "class" => "graphBackground"
 474          })
 475 
 476          # Axis
 477          @graph.add_element( "path", {
 478            "d" => "M 0 0 v#@graph_height",
 479            "class" => "axis",
 480            "id" => "xAxis"
 481          })
 482          @graph.add_element( "path", {
 483            "d" => "M 0 #@graph_height h#@graph_width",
 484            "class" => "axis",
 485            "id" => "yAxis"
 486          })
 487 
 488          draw_x_labels
 489          draw_y_labels
 490        end
 491 
 492 
 493        # Where in the X area the label is drawn
 494        # Centered in the field, should be width/2.  Start, 0.
 495        def x_label_offset( width )
 496          0
 497        end
 498 
 499        def make_datapoint_text( x, y, value, style="" )
 500          if show_data_values
 501            @foreground.add_element( "text", {
 502              "x" => x.to_s,
 503              "y" => y.to_s,
 504              "class" => "dataPointLabel",
 505              "style" => "#{style} stroke: #fff; stroke-width: 2;"
 506            }).text = value.to_s
 507            text = @foreground.add_element( "text", {
 508              "x" => x.to_s,
 509              "y" => y.to_s,
 510              "class" => "dataPointLabel"
 511            })
 512            text.text = value.to_s
 513            text.attributes["style"] = style if style.length > 0
 514          end
 515        end
 516 
 517 
 518        # Draws the X axis labels
 519        def draw_x_labels
 520          stagger = x_label_font_size + 5
 521          if show_x_labels
 522            label_width = field_width
 523 
 524            count = 0
 525            for label in get_x_labels
 526              if step_include_first_x_label == true then
 527                step = count % step_x_labels
 528              else
 529                step = (count + 1) % step_x_labels
 530              end
 531 
 532              if step == 0 then
 533                text = @graph.add_element( "text" )
 534                text.attributes["class"] = "xAxisLabels"
 535                text.text = label.to_s
 536 
 537                x = count * label_width + x_label_offset( label_width )
 538                y = @graph_height + x_label_font_size + 3
 539                t = 0 - (font_size / 2)
 540 
 541                if stagger_x_labels and count % 2 == 1
 542                  y += stagger
 543                  @graph.add_element( "path", {
 544                    "d" => "M#{x} #@graph_height v#{stagger}",
 545                    "class" => "staggerGuideLine"
 546                  })
 547                end
 548 
 549                text.attributes["x"] = x.to_s
 550                text.attributes["y"] = y.to_s
 551                if rotate_x_labels
 552                  text.attributes["transform"] = 
 553                    "rotate( 90 #{x} #{y-x_label_font_size} )"+
 554                    " translate( 0 -#{x_label_font_size/4} )"
 555                  text.attributes["style"] = "text-anchor: start"
 556                else
 557                  text.attributes["style"] = "text-anchor: middle"
 558                end
 559              end
 560 
 561              draw_x_guidelines( label_width, count ) if show_x_guidelines
 562              count += 1
 563            end
 564          end
 565        end
 566 
 567 
 568        # Where in the Y area the label is drawn
 569        # Centered in the field, should be width/2.  Start, 0.
 570        def y_label_offset( height )
 571          0
 572        end
 573 
 574 
 575        def field_width
 576          (@graph_width.to_f - font_size*2*right_font) /
 577             (get_x_labels.length - right_align)
 578        end
 579 
 580 
 581        def field_height
 582          (@graph_height.to_f - font_size*2*top_font) /
 583             (get_y_labels.length - top_align)
 584        end
 585 
 586 
 587        # Draws the Y axis labels
 588        def draw_y_labels
 589          stagger = y_label_font_size + 5
 590          if show_y_labels
 591            label_height = field_height
 592 
 593            count = 0
 594            y_offset = @graph_height + y_label_offset( label_height )
 595            y_offset += font_size/1.2 unless rotate_y_labels
 596            for label in get_y_labels
 597              y = y_offset - (label_height * count)
 598              x = rotate_y_labels ? 0 : -3
 599 
 600              if stagger_y_labels and count % 2 == 1
 601                x -= stagger
 602                @graph.add_element( "path", {
 603                  "d" => "M#{x} #{y} h#{stagger}",
 604                  "class" => "staggerGuideLine"
 605                })
 606              end
 607 
 608              text = @graph.add_element( "text", {
 609                "x" => x.to_s,
 610                "y" => y.to_s,
 611                "class" => "yAxisLabels"
 612              })
 613              text.text = label.to_s
 614              if rotate_y_labels
 615                text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
 616                  "rotate( 90 #{x} #{y} ) "
 617                text.attributes["style"] = "text-anchor: middle"
 618              else
 619                text.attributes["y"] = (y - (y_label_font_size/2)).to_s
 620                text.attributes["style"] = "text-anchor: end"
 621              end
 622              draw_y_guidelines( label_height, count ) if show_y_guidelines
 623              count += 1
 624            end
 625          end
 626        end
 627 
 628 
 629        # Draws the X axis guidelines
 630        def draw_x_guidelines( label_height, count )
 631          if count != 0
 632            @graph.add_element( "path", {
 633              "d" => "M#{label_height*count} 0 v#@graph_height",
 634              "class" => "guideLines"
 635            })
 636          end
 637        end
 638 
 639 
 640        # Draws the Y axis guidelines
 641        def draw_y_guidelines( label_height, count )
 642          if count != 0
 643            @graph.add_element( "path", {
 644              "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
 645              "class" => "guideLines"
 646            })
 647          end
 648        end
 649 
 650 
 651        # Draws the graph title and subtitle
 652        def draw_titles
 653          if show_graph_title
 654            @root.add_element( "text", {
 655              "x" => (width / 2).to_s,
 656              "y" => (title_font_size).to_s,
 657              "class" => "mainTitle"
 658            }).text = graph_title.to_s
 659          end
 660 
 661          if show_graph_subtitle
 662            y_subtitle = show_graph_title ? 
 663              title_font_size + 10 :
 664              subtitle_font_size
 665            @root.add_element("text", {
 666              "x" => (width / 2).to_s,
 667              "y" => (y_subtitle).to_s,
 668              "class" => "subTitle"
 669            }).text = graph_subtitle.to_s
 670          end
 671 
 672          if show_x_title
 673            y = @graph_height + @border_top + x_title_font_size
 674            if show_x_labels
 675              y += x_label_font_size + 5 if stagger_x_labels
 676              y += x_label_font_size + 5
 677            end
 678            x = width / 2
 679 
 680            @root.add_element("text", {
 681              "x" => x.to_s,
 682              "y" => y.to_s,
 683              "class" => "xAxisTitle",
 684            }).text = x_title.to_s
 685          end
 686 
 687          if show_y_title
 688            x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
 689            y = height / 2
 690 
 691            text = @root.add_element("text", {
 692              "x" => x.to_s,
 693              "y" => y.to_s,
 694              "class" => "yAxisTitle",
 695            })
 696            text.text = y_title.to_s
 697            if y_title_text_direction == :bt
 698              text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
 699            else
 700              text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
 701            end
 702          end
 703        end
 704 
 705        def keys 
 706          return @data.collect{ |d| d[:title] }
 707        end
 708 
 709        # Draws the legend on the graph
 710        def draw_legend
 711          if key
 712            group = @root.add_element( "g" )
 713 
 714            key_count = 0
 715            for key_name in keys
 716              y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
 717              group.add_element( "rect", {
 718                "x" => 0.to_s,
 719                "y" => y_offset.to_s,
 720                "width" => KEY_BOX_SIZE.to_s,
 721                "height" => KEY_BOX_SIZE.to_s,
 722                "class" => "key#{key_count+1}"
 723              })
 724              group.add_element( "text", {
 725                "x" => (KEY_BOX_SIZE + 5).to_s,
 726                "y" => (y_offset + KEY_BOX_SIZE).to_s,
 727                "class" => "keyText"
 728              }).text = key_name.to_s
 729              key_count += 1
 730            end
 731 
 732            case key_position
 733            when :right
 734              x_offset = @graph_width + @border_left + 10
 735              y_offset = @border_top + 20
 736            when :bottom
 737              x_offset = @border_left + 20
 738              y_offset = @border_top + @graph_height + 5
 739              if show_x_labels
 740                            max_x_label_height_px = (not rotate_x_labels) ? 
 741                                  x_label_font_size :
 742                                  get_x_labels.max{|a,b| 
 743                                    a.to_s.length<=>b.to_s.length
 744                                  }.to_s.length * x_label_font_size * 0.6
 745                  x_label_font_size
 746                y_offset += max_x_label_height_px
 747                y_offset += max_x_label_height_px + 5 if stagger_x_labels
 748              end
 749              y_offset += x_title_font_size + 5 if show_x_title
 750            end
 751            group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
 752          end
 753        end
 754 
 755 
 756        private
 757 
 758        def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
 759          if lo < hi
 760            p = partition(arrys,lo,hi)
 761            sort_multiple(arrys, lo, p-1)
 762            sort_multiple(arrys, p+1, hi)
 763          end
 764          arrys 
 765        end
 766 
 767        def partition( arrys, lo, hi )
 768          p = arrys[0][lo]
 769          l = lo
 770          z = lo+1
 771          while z <= hi
 772            if arrys[0][z] < p
 773              l += 1
 774              arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
 775            end
 776            z += 1
 777          end
 778          arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
 779          l
 780        end
 781 
 782        def style
 783          if no_css
 784            styles = parse_css
 785            @root.elements.each("//*[@class]") { |el|
 786              cl = el.attributes["class"]
 787              style = styles[cl]
 788              style += el.attributes["style"] if el.attributes["style"]
 789              el.attributes["style"] = style
 790            }
 791          end
 792        end
 793 
 794        def parse_css
 795          css = get_style
 796          rv = {}
 797          while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
 798            names_orig = names = $1
 799            css = $'
 800            css =~ /([^}]+)\}/m
 801            content = $1
 802            css = $'
 803 
 804            nms = []
 805            while names =~ /^\s*,?\s*\.(\w+)/
 806              nms << $1
 807              names = $'
 808            end
 809 
 810            content = content.tr( "\n\t", " ")
 811            for name in nms
 812              current = rv[name]
 813              current = current ? current+"; "+content : content
 814              rv[name] = current.strip.squeeze(" ")
 815            end
 816          end
 817          return rv
 818        end
 819 
 820 
 821        # Override and place code to add defs here
 822        def add_defs defs
 823        end
 824 
 825 
 826        def start_svg
 827          # Base document
 828          @doc = Document.new
 829          @doc << XMLDecl.new
 830          @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
 831            %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
 832          if style_sheet && style_sheet != ''
 833            @doc << Instruction.new( "xml-stylesheet",
 834              %Q{href="#{style_sheet}" type="text/css"} )
 835          end
 836          @root = @doc.add_element( "svg", {
 837            "width" => width.to_s,
 838            "height" => height.to_s,
 839            "viewBox" => "0 0 #{width} #{height}",
 840            "xmlns" => "http://www.w3.org/2000/svg",
 841            "xmlns:xlink" => "http://www.w3.org/1999/xlink",
 842            "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
 843            "a3:scriptImplementation" => "Adobe"
 844          })
 845          @root << Comment.new( " "+"\\"*66 )
 846          @root << Comment.new( " Created with SVG::Graph " )
 847          @root << Comment.new( " SVG::Graph by Sean E. Russell " )
 848          @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
 849          " Leo Lapworth & Stephan Morgan " )
 850          @root << Comment.new( " "+"/"*66 )
 851 
 852          defs = @root.add_element( "defs" )
 853          add_defs defs
 854          if not(style_sheet && style_sheet != '') and !no_css
 855            @root << Comment.new(" include default stylesheet if none specified ")
 856            style = defs.add_element( "style", {"type"=>"text/css"} )
 857            style << CData.new( get_style )
 858          end
 859 
 860          @root << Comment.new( "SVG Background" )
 861          @root.add_element( "rect", {
 862            "width" => width.to_s,
 863            "height" => height.to_s,
 864            "x" => "0",
 865            "y" => "0",
 866            "class" => "svgBackground"
 867          })
 868        end
 869 
 870 
 871        def calculate_graph_dimensions
 872          calculate_left_margin
 873          calculate_right_margin
 874          calculate_bottom_margin
 875          calculate_top_margin
 876          @graph_width = width - @border_left - @border_right
 877          @graph_height = height - @border_top - @border_bottom
 878        end
 879 
 880        def get_style
 881          return <<EOL
 882  /* Copy from here for external style sheet */
 883  .svgBackground{
 884    fill:#ffffff;
 885  }
 886  .graphBackground{
 887    fill:#f0f0f0;
 888  }
 889 
 890  /* graphs titles */
 891  .mainTitle{
 892    text-anchor: middle;
 893    fill: #000000;
 894    font-size: #{title_font_size}px;
 895    font-family: "Arial", sans-serif;
 896    font-weight: normal;
 897  }
 898  .subTitle{
 899    text-anchor: middle;
 900    fill: #999999;
 901    font-size: #{subtitle_font_size}px;
 902    font-family: "Arial", sans-serif;
 903    font-weight: normal;
 904  }
 905 
 906  .axis{
 907    stroke: #000000;
 908    stroke-width: 1px;
 909  }
 910 
 911  .guideLines{
 912    stroke: #666666;
 913    stroke-width: 1px;
 914    stroke-dasharray: 5 5;
 915  }
 916 
 917  .xAxisLabels{
 918    text-anchor: middle;
 919    fill: #000000;
 920    font-size: #{x_label_font_size}px;
 921    font-family: "Arial", sans-serif;
 922    font-weight: normal;
 923  }
 924 
 925  .yAxisLabels{
 926    text-anchor: end;
 927    fill: #000000;
 928    font-size: #{y_label_font_size}px;
 929    font-family: "Arial", sans-serif;
 930    font-weight: normal;
 931  }
 932 
 933  .xAxisTitle{
 934    text-anchor: middle;
 935    fill: #ff0000;
 936    font-size: #{x_title_font_size}px;
 937    font-family: "Arial", sans-serif;
 938    font-weight: normal;
 939  }
 940 
 941  .yAxisTitle{
 942    fill: #ff0000;
 943    text-anchor: middle;
 944    font-size: #{y_title_font_size}px;
 945    font-family: "Arial", sans-serif;
 946    font-weight: normal;
 947  }
 948 
 949  .dataPointLabel{
 950    fill: #000000;
 951    text-anchor:middle;
 952    font-size: 10px;
 953    font-family: "Arial", sans-serif;
 954    font-weight: normal;
 955  }
 956 
 957  .staggerGuideLine{
 958    fill: none;
 959    stroke: #000000;
 960    stroke-width: 0.5px;  
 961  }
 962 
 963  #{get_css}
 964 
 965  .keyText{
 966    fill: #000000;
 967    text-anchor:start;
 968    font-size: #{key_font_size}px;
 969    font-family: "Arial", sans-serif;
 970    font-weight: normal;
 971  }
 972  /* End copy for external style sheet */
 973  EOL
 974        end
 975 
 976      end
 977    end
 978  end