File: lib/SVG/Graph/Pie.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: SVG#3
  module: Graph#4
  class: Pie#57
inherits from
  Graph ( SVG::Graph )
has properties
method: set_defaults #73
method: add_data #111
attribute: show_shadow [RW] #119
attribute: shadow_offset [RW] #121
attribute: show_data_labels [RW] #123
attribute: show_actual_values [RW] #125
attribute: show_percent [RW] #128
attribute: show_key_data_labels [RW] #130
attribute: show_key_actual_values [RW] #132
attribute: show_key_percent [RW] #134
attribute: expanded [RW] #136
attribute: expand_greatest [RW] #138
attribute: expand_gap [RW] #140
attribute: datapoint_font_size [RW] #142
method: add_defs #147
method: draw_graph #160
method: get_y_labels #163
method: get_x_labels #167
method: keys #171
constant: RADIANS #185
method: draw_data #187
method: round #302
method: get_css #308

Class Hierarchy

Object ( Builtin-Module )
Graph ( SVG::Graph )
  Pie    #57

Code

   1  require 'SVG/Graph/Graph'
   2 
   3  module SVG
   4    module Graph
   5      # === Create presentation quality SVG pie graphs easily
   6      # 
   7      # == Synopsis
   8      # 
   9      #   require 'SVG/Graph/Pie'
  10      # 
  11      #   fields = %w(Jan Feb Mar)
  12      #   data_sales_02 = [12, 45, 21]
  13      #   
  14      #   graph = SVG::Graph::Pie.new({
  15      #           :height => 500,
  16      #     :width  => 300,
  17      #     :fields => fields,
  18      #   })
  19      #   
  20      #   graph.add_data({
  21      #           :data => data_sales_02,
  22      #     :title => 'Sales 2002',
  23      #   })
  24      #   
  25      #   print "Content-type: image/svg+xml\r\n\r\n"
  26      #   print graph.burn();
  27      # 
  28      # == Description
  29      # 
  30      # This object aims to allow you to easily create high quality
  31      # SVG pie graphs. You can either use the default style sheet
  32      # or supply your own. Either way there are many options which can
  33      # be configured to give you control over how the graph is
  34      # generated - with or without a key, display percent on pie chart,
  35      # title, subtitle etc.
  36      #
  37      # = Examples
  38      # 
  39      # http://www.germane-software/repositories/public/SVG/test/single.rb
  40      # 
  41      # == See also
  42      #
  43      # * SVG::Graph::Graph
  44      # * SVG::Graph::BarHorizontal
  45      # * SVG::Graph::Bar
  46      # * SVG::Graph::Line
  47      # * SVG::Graph::Plot
  48      # * SVG::Graph::TimeSeries
  49      #
  50      # == Author
  51      #
  52      # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
  53      #
  54      # Copyright 2004 Sean E. Russell
  55      # This software is available under the Ruby license[LICENSE.txt]
  56      #
  57      class Pie < Graph
  58        # Defaults are those set by Graph::initialize, and
  59        # [show_shadow] true
  60        # [shadow_offset] 10
  61        # [show_data_labels] false
  62        # [show_actual_values] false
  63        # [show_percent] true
  64        # [show_key_data_labels] true
  65        # [show_key_actual_values] true
  66        # [show_key_percent] false
  67        # [expanded] false
  68        # [expand_greatest] false
  69        # [expand_gap] 10
  70        # [show_x_labels] false
  71        # [show_y_labels] false
  72        # [datapoint_font_size] 12
  73        def set_defaults
  74          init_with(
  75            :show_shadow                  => true,
  76            :shadow_offset                => 10, 
  77            
  78            :show_data_labels           => false,
  79            :show_actual_values     => false,
  80            :show_percent                 => true,
  81 
  82            :show_key_data_labels   => true,
  83            :show_key_actual_values => true,
  84            :show_key_percent                 => false,
  85            
  86            :expanded                                     => false,
  87            :expand_greatest                  => false,
  88            :expand_gap             => 10,
  89            
  90            :show_x_labels          => false,
  91            :show_y_labels          => false,
  92            :datapoint_font_size    => 12
  93          )
  94          @data = []
  95        end
  96 
  97        # Adds a data set to the graph.
  98        #
  99        #   graph.add_data( { :data => [1,2,3,4] } )
 100        #
 101        # Note that the :title is not necessary.  If multiple
 102        # data sets are added to the graph, the pie chart will
 103        # display the +sums+ of the data.  EG:
 104        #
 105        #   graph.add_data( { :data => [1,2,3,4] } )
 106        #   graph.add_data( { :data => [2,3,5,9] } )
 107        #
 108        # is the same as:
 109        #
 110        #   graph.add_data( { :data => [3,5,8,13] } )
 111        def add_data arg
 112          arg[:data].each_index {|idx|
 113            @data[idx] = 0 unless @data[idx]
 114            @data[idx] += arg[:data][idx]
 115          }
 116        end
 117 
 118        # If true, displays a drop shadow for the chart
 119        attr_accessor :show_shadow 
 120        # Sets the offset of the shadow from the pie chart
 121        attr_accessor :shadow_offset
 122        # If true, display the data labels on the chart
 123        attr_accessor :show_data_labels 
 124        # If true, display the actual field values in the data labels
 125        attr_accessor :show_actual_values 
 126        # If true, display the percentage value of each pie wedge in the data
 127        # labels
 128        attr_accessor :show_percent
 129        # If true, display the labels in the key
 130        attr_accessor :show_key_data_labels 
 131        # If true, display the actual value of the field in the key
 132        attr_accessor :show_key_actual_values 
 133        # If true, display the percentage value of the wedges in the key
 134        attr_accessor :show_key_percent
 135        # If true, "explode" the pie (put space between the wedges)
 136        attr_accessor :expanded 
 137        # If true, expand the largest pie wedge
 138        attr_accessor :expand_greatest 
 139        # The amount of space between expanded wedges
 140        attr_accessor :expand_gap 
 141        # The font size of the data point labels
 142        attr_accessor :datapoint_font_size
 143 
 144 
 145        protected
 146 
 147        def add_defs defs
 148          gradient = defs.add_element( "filter", {
 149            "id"=>"dropshadow",
 150            "width" => "1.2",
 151            "height" => "1.2",
 152          } )
 153          gradient.add_element( "feGaussianBlur", {
 154            "stdDeviation" => "4",
 155            "result" => "blur"
 156          })
 157        end
 158 
 159        # We don't need the graph
 160        def draw_graph
 161        end
 162 
 163        def get_y_labels
 164          [""]
 165        end
 166 
 167        def get_x_labels
 168          [""]
 169        end
 170 
 171        def keys
 172          total = 0
 173          max_value = 0
 174          @data.each {|x| total += x }
 175          percent_scale = 100.0 / total
 176          count = -1
 177          a = @config[:fields].collect{ |x|
 178            count += 1
 179            v = @data[count]
 180            perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : ""
 181            x + " [" + v.to_s + "]" + perc
 182          }
 183        end
 184 
 185        RADIANS = Math::PI/180
 186 
 187        def draw_data
 188          @graph = @root.add_element( "g" )
 189          background = @graph.add_element("g")
 190          midground = @graph.add_element("g")
 191 
 192          diameter = @graph_height > @graph_width ? @graph_width : @graph_height
 193          diameter -= expand_gap if expanded or expand_greatest
 194          diameter -= datapoint_font_size if show_data_labels
 195          diameter -= 10 if show_shadow
 196          radius = diameter / 2.0
 197 
 198          xoff = (width - diameter) / 2
 199          yoff = (height - @border_bottom - diameter)
 200          yoff -= 10 if show_shadow
 201          @graph.attributes['transform'] = "translate( #{xoff} #{yoff} )"
 202 
 203          wedge_text_pad = 5
 204          wedge_text_pad = 20 if show_percent and show_data_labels
 205 
 206          total = 0
 207          max_value = 0
 208          @data.each {|x| 
 209            max_value = max_value < x ? x : max_value
 210            total += x 
 211          }
 212          percent_scale = 100.0 / total
 213 
 214          prev_percent = 0
 215          rad_mult = 3.6 * RADIANS
 216          @config[:fields].each_index { |count|
 217            value = @data[count]
 218            percent = percent_scale * value
 219 
 220            radians = prev_percent * rad_mult
 221            x_start = radius+(Math.sin(radians) * radius)
 222            y_start = radius-(Math.cos(radians) * radius)
 223            radians = (prev_percent+percent) * rad_mult
 224            x_end = radius+(Math.sin(radians) * radius)
 225            x_end -= 0.00001 if @data.length == 1
 226            y_end = radius-(Math.cos(radians) * radius)
 227            path = "M#{radius},#{radius} L#{x_start},#{y_start} "+
 228              "A#{radius},#{radius} "+
 229              "0, #{percent >= 50 ? '1' : '0'},1, "+
 230              "#{x_end} #{y_end} Z"
 231 
 232 
 233            wedge = @foreground.add_element( "path", {
 234              "d" => path,
 235              "class" => "fill#{count+1}"
 236            })
 237 
 238            translate = nil
 239            tx = 0
 240            ty = 0
 241            half_percent = prev_percent + percent / 2
 242            radians = half_percent * rad_mult
 243 
 244            if show_shadow
 245              shadow = background.add_element( "path", {
 246                "d" => path,
 247                "filter" => "url(#dropshadow)",
 248                "style" => "fill: #ccc; stroke: none;"
 249              })
 250              clear = midground.add_element( "path", {
 251                "d" => path,
 252                "style" => "fill: #fff; stroke: none;"
 253              })
 254            end
 255 
 256            if expanded or (expand_greatest && value == max_value)
 257              tx = (Math.sin(radians) * expand_gap)
 258              ty = -(Math.cos(radians) * expand_gap)
 259              translate = "translate( #{tx} #{ty} )"
 260              wedge.attributes["transform"] = translate
 261              clear.attributes["transform"] = translate if clear
 262            end
 263 
 264            if show_shadow
 265              shadow.attributes["transform"] = 
 266                "translate( #{tx+shadow_offset} #{ty+shadow_offset} )"
 267            end
 268            
 269            if show_data_labels and value != 0
 270              label = ""
 271              label += @config[:fields][count] if show_key_data_labels
 272              label += " ["+value.to_s+"]" if show_actual_values
 273              label += " "+percent.round.to_s+"%" if show_percent
 274 
 275              msr = Math.sin(radians)
 276              mcr = Math.cos(radians)
 277              tx = radius + (msr * radius)
 278              ty = radius -(mcr * radius)
 279 
 280              if expanded or (expand_greatest && value == max_value)
 281                tx += (msr * expand_gap)
 282                ty -= (mcr * expand_gap)
 283              end
 284              @foreground.add_element( "text", {
 285                "x" => tx.to_s,
 286                "y" => ty.to_s,
 287                "class" => "dataPointLabel",
 288                "style" => "stroke: #fff; stroke-width: 2;"
 289              }).text = label.to_s
 290              @foreground.add_element( "text", {
 291                "x" => tx.to_s,
 292                "y" => ty.to_s,
 293                "class" => "dataPointLabel",
 294              }).text = label.to_s
 295            end
 296 
 297            prev_percent += percent
 298          }
 299        end
 300        
 301 
 302        def round val, to
 303          up = 10**to.to_f
 304          (val * up).to_i / up
 305        end
 306 
 307 
 308        def get_css
 309          return <<EOL
 310  .dataPointLabel{
 311          fill: #000000;
 312          text-anchor:middle;
 313          font-size: #{datapoint_font_size}px;
 314          font-family: "Arial", sans-serif;
 315          font-weight: normal;
 316  }
 317 
 318  /* key - MUST match fill styles */
 319  .key1,.fill1{
 320          fill: #ff0000;
 321          fill-opacity: 0.7;
 322          stroke: none;
 323          stroke-width: 1px;
 324  }
 325  .key2,.fill2{
 326          fill: #0000ff;
 327          fill-opacity: 0.7;
 328          stroke: none;
 329          stroke-width: 1px;
 330  }
 331  .key3,.fill3{
 332          fill-opacity: 0.7;
 333          fill: #00ff00;
 334          stroke: none;
 335          stroke-width: 1px;
 336  }
 337  .key4,.fill4{
 338          fill-opacity: 0.7;
 339          fill: #ffcc00;
 340          stroke: none;
 341          stroke-width: 1px;
 342  }
 343  .key5,.fill5{
 344          fill-opacity: 0.7;
 345          fill: #00ccff;
 346          stroke: none;
 347          stroke-width: 1px;
 348  }
 349  .key6,.fill6{
 350          fill-opacity: 0.7;
 351          fill: #ff00ff;
 352          stroke: none;
 353          stroke-width: 1px;
 354  }
 355  .key7,.fill7{
 356          fill-opacity: 0.7;
 357          fill: #00ff99;
 358          stroke: none;
 359          stroke-width: 1px;
 360  }
 361  .key8,.fill8{
 362          fill-opacity: 0.7;
 363          fill: #ffff00;
 364          stroke: none;
 365          stroke-width: 1px;
 366  }
 367  .key9,.fill9{
 368          fill-opacity: 0.7;
 369          fill: #cc6666;
 370          stroke: none;
 371          stroke-width: 1px;
 372  }
 373  .key10,.fill10{
 374          fill-opacity: 0.7;
 375          fill: #663399;
 376          stroke: none;
 377          stroke-width: 1px;
 378  }
 379  .key11,.fill11{
 380          fill-opacity: 0.7;
 381          fill: #339900;
 382          stroke: none;
 383          stroke-width: 1px;
 384  }
 385  .key12,.fill12{
 386          fill-opacity: 0.7;
 387          fill: #9966FF;
 388          stroke: none;
 389          stroke-width: 1px;
 390  }
 391  EOL
 392        end
 393      end
 394    end
 395  end