From 899ad07910a7d7612d9258ee62a76f902324a081 Mon Sep 17 00:00:00 2001
From: Per Cederqvist <ceder@lysator.liu.se>
Date: Sun, 20 Aug 2006 13:07:33 +0000
Subject: [PATCH] ./script/plugin install http://topfunky.net/svn/plugins/gruff

---
 vendor/plugins/gruff/MIT-LICENSE              |  20 +
 vendor/plugins/gruff/README                   |  15 +
 vendor/plugins/gruff/Rakefile                 |  23 +
 vendor/plugins/gruff/about.yml                |   7 +
 .../gruff/generators/gruff/gruff_generator.rb |  63 ++
 .../generators/gruff/templates/controller.rb  |  32 +
 .../gruff/templates/functional_test.rb        |  24 +
 vendor/plugins/gruff/lib/gruff.rb             |  15 +
 vendor/plugins/gruff/lib/gruff/area.rb        |  58 ++
 vendor/plugins/gruff/lib/gruff/bar.rb         |  66 ++
 .../plugins/gruff/lib/gruff/bar_conversion.rb |  55 ++
 vendor/plugins/gruff/lib/gruff/base.rb        | 751 ++++++++++++++++++
 vendor/plugins/gruff/lib/gruff/line.rb        |  95 +++
 vendor/plugins/gruff/lib/gruff/net.rb         | 133 ++++
 vendor/plugins/gruff/lib/gruff/photo_bar.rb   | 100 +++
 vendor/plugins/gruff/lib/gruff/pie.rb         |  88 ++
 vendor/plugins/gruff/lib/gruff/scene.rb       | 196 +++++
 .../gruff/lib/gruff/side_stacked_bar.rb       | 119 +++
 vendor/plugins/gruff/lib/gruff/spider.rb      | 130 +++
 vendor/plugins/gruff/lib/gruff/stacked_bar.rb |  49 ++
 20 files changed, 2039 insertions(+)
 create mode 100644 vendor/plugins/gruff/MIT-LICENSE
 create mode 100644 vendor/plugins/gruff/README
 create mode 100644 vendor/plugins/gruff/Rakefile
 create mode 100644 vendor/plugins/gruff/about.yml
 create mode 100644 vendor/plugins/gruff/generators/gruff/gruff_generator.rb
 create mode 100644 vendor/plugins/gruff/generators/gruff/templates/controller.rb
 create mode 100644 vendor/plugins/gruff/generators/gruff/templates/functional_test.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff/area.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff/bar.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff/bar_conversion.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff/base.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff/line.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff/net.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff/photo_bar.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff/pie.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff/scene.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff/side_stacked_bar.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff/spider.rb
 create mode 100644 vendor/plugins/gruff/lib/gruff/stacked_bar.rb

diff --git a/vendor/plugins/gruff/MIT-LICENSE b/vendor/plugins/gruff/MIT-LICENSE
new file mode 100644
index 0000000..8d1b480
--- /dev/null
+++ b/vendor/plugins/gruff/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2006 Geoffrey Grosenbach
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/plugins/gruff/README b/vendor/plugins/gruff/README
new file mode 100644
index 0000000..db98db5
--- /dev/null
+++ b/vendor/plugins/gruff/README
@@ -0,0 +1,15 @@
+== Gruff Plugin
+
+Make pretty graphs.
+
+Also has a "gruff" generator for copying a controller and functional test to your app.
+
+  ./script/generate gruff Reports
+
+See examples at http://nubyonrails.com/pages/gruff
+
+== Author
+
+Geoffrey Grosenbach boss@topfunky.com http://nubyonrails.com
+
+
diff --git a/vendor/plugins/gruff/Rakefile b/vendor/plugins/gruff/Rakefile
new file mode 100644
index 0000000..d3456e4
--- /dev/null
+++ b/vendor/plugins/gruff/Rakefile
@@ -0,0 +1,23 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the plugin.'
+Rake::TestTask.new(:test) do |t|
+  t.libs << 'lib'
+  t.pattern = 'test/**/test_*.rb'
+  t.verbose = true
+end
+
+desc 'Generate documentation for the plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+  rdoc.rdoc_dir = 'rdoc'
+  rdoc.title    = 'CalendarHelper'
+  rdoc.options << '--line-numbers' << '--inline-source'
+  rdoc.rdoc_files.include('README')
+  rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
diff --git a/vendor/plugins/gruff/about.yml b/vendor/plugins/gruff/about.yml
new file mode 100644
index 0000000..874f85f
--- /dev/null
+++ b/vendor/plugins/gruff/about.yml
@@ -0,0 +1,7 @@
+author: topfunky
+summary: Make graphs. Requires RMagick. Includes a generator for a graphics controller, and tests.
+homepage: http://nubyonrails.com/pages/gruff
+plugin: http://topfunky.net/svn/plugins/gruff
+license: MIT
+version: 0.1.2
+rails_version: 1.0+
diff --git a/vendor/plugins/gruff/generators/gruff/gruff_generator.rb b/vendor/plugins/gruff/generators/gruff/gruff_generator.rb
new file mode 100644
index 0000000..337f58e
--- /dev/null
+++ b/vendor/plugins/gruff/generators/gruff/gruff_generator.rb
@@ -0,0 +1,63 @@
+class GruffGenerator < Rails::Generator::NamedBase
+
+  attr_reader   :controller_name,
+                :controller_class_path,
+                :controller_file_path,
+                :controller_class_nesting,
+                :controller_class_nesting_depth,
+                :controller_class_name,
+                :controller_singular_name,
+                :controller_plural_name,
+                :parent_folder_for_require
+  alias_method  :controller_file_name,  :controller_singular_name
+  alias_method  :controller_table_name, :controller_plural_name
+
+  def initialize(runtime_args, runtime_options = {})
+    super
+
+    # Take controller name from the next argument.
+    @controller_name = runtime_args.shift
+
+    base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@controller_name)
+    @controller_class_name_without_nesting, @controller_singular_name, @controller_plural_name = inflect_names(base_name)
+
+    if @controller_class_nesting.empty?
+      @controller_class_name = @controller_class_name_without_nesting
+    else
+      @controller_class_name = "#{@controller_class_nesting}::#{@controller_class_name_without_nesting}"
+    end    
+  end
+
+  def manifest
+    record do |m|
+      # Check for class naming collisions.
+      m.class_collisions controller_class_path, "#{controller_class_name}Controller",
+                                                "#{controller_class_name}ControllerTest"
+
+      # Controller, helper, views, and test directories.
+      m.directory File.join('app/controllers', controller_class_path)
+      m.directory File.join('test/functional', controller_class_path)
+
+      m.template 'controller.rb',
+                  File.join('app/controllers',
+                            controller_class_path,
+                            "#{controller_file_name}_controller.rb")
+
+      # For some reason this doesn't take effect if done in initialize()
+      @parent_folder_for_require = @controller_class_path.join('/').gsub(%r%app/controllers/?%, '')
+      @parent_folder_for_require += @parent_folder_for_require.blank? ? '' : '/'
+
+      m.template 'functional_test.rb',
+                  File.join('test/functional',
+                            controller_class_path,
+                            "#{controller_file_name}_controller_test.rb")
+
+    end
+  end
+
+  protected
+    # Override with your own usage banner.
+    def banner
+      "Usage: #{$0} gruff ControllerName"
+    end
+end
diff --git a/vendor/plugins/gruff/generators/gruff/templates/controller.rb b/vendor/plugins/gruff/generators/gruff/templates/controller.rb
new file mode 100644
index 0000000..b8f90dd
--- /dev/null
+++ b/vendor/plugins/gruff/generators/gruff/templates/controller.rb
@@ -0,0 +1,32 @@
+class <%= controller_class_name %>Controller < ApplicationController
+
+  # To make caching easier, add a line like this to config/routes.rb:
+  # map.graph "graph/:action/:id/image.png", :controller => "graph"
+  #
+  # Then reference it with the named route:
+  #   image_tag graph_url(:action => 'show', :id => 42)
+
+  def show
+    g = Gruff::Line.new
+    # Uncomment to use your own theme or font
+    # See http://colourlovers.com or http://www.firewheeldesign.com/widgets/ for color ideas
+#     g.theme = {
+#       :colors => ['#663366', '#cccc99', '#cc6633', '#cc9966', '#99cc99'],
+#       :marker_color => 'white',
+#       :background_colors => ['black', '#333333']
+#     }
+#     g.font = File.expand_path('artwork/fonts/VeraBd.ttf', RAILS_ROOT)
+
+    g.title = "Gruff-o-Rama"
+    
+    g.data("Apples", [1, 2, 3, 4, 4, 3])
+    g.data("Oranges", [4, 8, 7, 9, 8, 9])
+    g.data("Watermelon", [2, 3, 1, 5, 6, 8])
+    g.data("Peaches", [9, 9, 10, 8, 7, 9])
+
+    g.labels = {0 => '2004', 2 => '2005', 4 => '2006'}
+
+    send_data(g.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "gruff.png")
+  end
+
+end
diff --git a/vendor/plugins/gruff/generators/gruff/templates/functional_test.rb b/vendor/plugins/gruff/generators/gruff/templates/functional_test.rb
new file mode 100644
index 0000000..c1f0818
--- /dev/null
+++ b/vendor/plugins/gruff/generators/gruff/templates/functional_test.rb
@@ -0,0 +1,24 @@
+require File.dirname(__FILE__) + '<%= '/..' * controller_class_name.split("::").length %>/test_helper'
+require '<%= parent_folder_for_require %><%= controller_file_name %>_controller'
+
+# Re-raise errors caught by the controller.
+class <%= controller_class_name %>Controller; def rescue_action(e) raise e end; end
+
+class <%= controller_class_name %>ControllerTest < Test::Unit::TestCase
+
+  #fixtures :data
+
+  def setup
+    @controller = <%= controller_class_name %>Controller.new
+    @request    = ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+  end
+
+  # TODO Replace this with your actual tests
+  def test_show
+    get :show
+    assert_response :success
+    assert_equal 'image/png', @response.headers['Content-Type']
+  end
+  
+end
diff --git a/vendor/plugins/gruff/lib/gruff.rb b/vendor/plugins/gruff/lib/gruff.rb
new file mode 100644
index 0000000..9866852
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff.rb
@@ -0,0 +1,15 @@
+# Extra full path added to fix some require errors on some installations.
+
+require File.dirname(__FILE__) + '/gruff/base'
+
+require File.dirname(__FILE__) + '/gruff/area'
+require File.dirname(__FILE__) + '/gruff/bar'
+require File.dirname(__FILE__) + '/gruff/line'
+require File.dirname(__FILE__) + '/gruff/pie'
+require File.dirname(__FILE__) + '/gruff/spider'
+require File.dirname(__FILE__) + '/gruff/net'
+require File.dirname(__FILE__) + '/gruff/stacked_bar'
+require File.dirname(__FILE__) + '/gruff/side_stacked_bar'
+require File.dirname(__FILE__) + '/gruff/photo_bar'
+
+require File.dirname(__FILE__) + '/gruff/scene'
diff --git a/vendor/plugins/gruff/lib/gruff/area.rb b/vendor/plugins/gruff/lib/gruff/area.rb
new file mode 100644
index 0000000..323aff1
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff/area.rb
@@ -0,0 +1,58 @@
+
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::Area < Gruff::Base
+
+  def draw
+    super
+
+    return unless @has_data
+
+    @x_increment = @graph_width / (@column_count - 1).to_f
+    @d = @d.stroke 'transparent'
+
+    @norm_data.each do |data_row|
+      poly_points = Array.new
+      prev_x = prev_y = 0.0
+      @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+      data_row[1].each_with_index do |data_point, index|
+        # Use incremented x and scaled y
+        new_x = @graph_left + (@x_increment * index)
+        new_y = @graph_top + (@graph_height - data_point * @graph_height)
+
+        if prev_x > 0 and prev_y > 0 then
+          poly_points << new_x
+          poly_points << new_y
+          
+          #@d = @d.polyline(prev_x, prev_y, new_x, new_y)
+        else
+          poly_points << @graph_left
+          poly_points << @graph_bottom - 1
+          poly_points << new_x
+          poly_points << new_y
+          
+          #@d = @d.polyline(@graph_left, @graph_bottom, new_x, new_y)
+        end
+
+        draw_label(new_x, index)
+
+        prev_x = new_x
+        prev_y = new_y
+      end
+
+      # Add closing points, draw polygon
+      poly_points << @graph_right
+      poly_points << @graph_bottom - 1
+      poly_points << @graph_left
+      poly_points << @graph_bottom - 1
+
+      @d = @d.polyline(*poly_points)
+
+    end
+
+    @d.draw(@base_image)
+  end
+   
+ 
+end
diff --git a/vendor/plugins/gruff/lib/gruff/bar.rb b/vendor/plugins/gruff/lib/gruff/bar.rb
new file mode 100644
index 0000000..88b8f91
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff/bar.rb
@@ -0,0 +1,66 @@
+
+require File.dirname(__FILE__) + '/base'
+require File.dirname(__FILE__) + '/bar_conversion'
+
+class Gruff::Bar < Gruff::Base
+
+  def draw
+    super
+    return unless @has_data
+
+    # Setup spacing.
+    #
+    # Columns sit side-by-side.
+    spacing_factor = 0.9
+    @bar_width = @graph_width / (@column_count * @data.length).to_f
+
+    @d = @d.stroke_opacity 0.0
+
+    # setup the BarConversion Object
+    conversion = Gruff::BarConversion.new()
+    conversion.graph_height = @graph_height
+    conversion.graph_top = @graph_top
+    # set up the right mode [1,2,3] see BarConversion for further explains
+    if @minimum_value >= 0 then
+      # all bars go from zero to positiv
+      conversion.mode = 1
+    else
+      # all bars go from 0 to negativ
+      if @maximum_value <= 0 then
+        conversion.mode = 2
+      else
+        # bars either go from zero to negativ or to positiv
+        conversion.mode = 3
+        conversion.spread = @spread
+        conversion.minimum_value = @minimum_value
+        conversion.zero = -@minimum_value/@spread
+      end
+    end
+
+    # iterate over all normalised data
+    @norm_data.each_with_index do |data_row, row_index|
+      @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+      data_row[1].each_with_index do |data_point, point_index|
+        # Use incremented x and scaled y
+        # x
+        left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index)))
+        right_x = left_x + @bar_width * spacing_factor
+        # y
+        conv = []
+        conversion.getLeftYRightYscaled( data_point, conv )
+
+        # create new bar
+        @d = @d.rectangle(left_x, conv[0], right_x, conv[1])
+
+        # Calculate center based on bar_width and current row
+        label_center = @graph_left + (@data.length * @bar_width * point_index) + (@data.length * @bar_width / 2.0)
+        draw_label(label_center, point_index)
+      end
+
+    end
+
+    @d.draw(@base_image)    
+  end
+
+end
diff --git a/vendor/plugins/gruff/lib/gruff/bar_conversion.rb b/vendor/plugins/gruff/lib/gruff/bar_conversion.rb
new file mode 100644
index 0000000..d632944
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff/bar_conversion.rb
@@ -0,0 +1,55 @@
+##############################################################################
+#
+#  Ruby Gruff/bar_conversion 
+#
+#  Copyright: David Stokar
+#
+#  Date Written: 2006/01/27
+#
+#  $Revision: 0.2 $
+#
+#  $Log: bar_conversion.rb $
+#  $Log: Added comments $
+##############################################################################
+
+#
+#	This class perfoms the y coordinats conversion for the bar class
+#	There are 3 cases: 1. Bars all go from zero in positiv direction
+#					   2. Bars all go from zero to negativ direction	
+#					   3. Bars either go from zero to positiv or from zero to negativ	
+#
+class Gruff::BarConversion
+	attr_writer :mode
+	attr_writer :zero
+	attr_writer :graph_top
+	attr_writer :graph_height
+	attr_writer :minimum_value
+	attr_writer :spread
+	
+	def getLeftYRightYscaled( data_point, result )
+		case @mode
+		when 1 then # Case one
+			# minimum value >= 0 ( only positiv values )
+      result[0] = @graph_top + @graph_height*(1 - data_point) + 1
+  		result[1] = @graph_top + @graph_height - 1
+		when 2 then  # Case two
+			# only negativ values
+   		result[0] = @graph_top + 1
+  		result[1] = @graph_top + @graph_height*(1 - data_point) - 1
+		when 3 then # Case three
+			# positiv and negativ values
+    	val = data_point-@minimum_value/@spread
+    	if ( data_point >= @zero ) then
+    		result[0] = @graph_top + @graph_height*(1 - (val-@zero)) + 1
+	    	result[1] = @graph_top + @graph_height*(1 - @zero) - 1
+    	else
+				result[0] = @graph_top + @graph_height*(1 - (val-@zero)) + 1
+	    	result[1] = @graph_top + @graph_height*(1 - @zero) - 1
+    	end
+		else
+			result[0] = 0.0
+			result[1] = 0.0
+		end				
+	end	
+
+end
diff --git a/vendor/plugins/gruff/lib/gruff/base.rb b/vendor/plugins/gruff/lib/gruff/base.rb
new file mode 100644
index 0000000..5565dba
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff/base.rb
@@ -0,0 +1,751 @@
+#
+# = Gruff. Graphs.
+#
+# Author:: Geoffrey Grosenbach boss@topfunky.com
+#
+# Originally Created:: October 23, 2005
+#
+# Extra thanks to Tim Hunter for writing RMagick, 
+# and also contributions by
+# Jarkko Laine, Mike Perham, Andreas Schwarz, 
+# Alun Eyre, Guillaume Theoret, David Stokar, 
+# Paul Rogers, Dave Woodward, Frank Oxener,
+# Kevin Clark, Cies Breijs, Richard Cowin,
+# and a cast of thousands.
+#
+
+require 'RMagick'
+
+module Gruff
+  
+  VERSION = '0.1.2'
+  
+  class Base
+  
+    include Magick
+
+    DATA_LABEL_INDEX = 0
+    DATA_VALUES_INDEX = 1
+    DATA_COLOR_INDEX = 2
+
+    # A hash of names for the individual columns, where the key is the array index for the column this label represents.
+    #
+    # Not all columns need to be named.
+    #
+    # Example: 0 => 2005, 3 => 2006, 5 => 2007, 7 => 2008
+    attr_accessor :labels
+
+    # Get or set the list of colors that will be used to draw the bars or lines.
+    attr_accessor :colors
+
+    # The large title of the graph displayed at the top
+    attr_accessor :title
+
+    # Font used for titles, labels, etc. Works best if you provide the full path to the TTF font file.
+    # RMagick must be built with the Freetype libraries for this to work properly.
+    #
+    # Tries to find Bitstream Vera (Vera.ttf) in the location specified by
+    # ENV['MAGICK_FONT_PATH']. Uses default RMagick font otherwise.
+    attr_accessor :font
+
+    # Hide various elements
+    attr_accessor :hide_line_markers, :hide_legend, :hide_title
+
+    # Message shown when there is no data. Fits up to 20 characters. Defaults to "No Data."
+    attr_accessor :no_data_message
+
+    # Optionally set the size of the font. Based on an 800x600px graph. Default is 20.
+    #
+    # Will be scaled down if graph is smaller than 800px wide.
+    attr_accessor :legend_font_size
+
+    # The font size of the labels around the graph
+    attr_accessor :marker_font_size
+    
+    # The color of the auxiliary labels and lines
+    attr_accessor :marker_color
+    
+    # The font size of the large title at the top of the graph
+    attr_accessor :title_font_size
+
+    # You can manually set a minimum value instead of having the values guessed for you.
+    #
+    # Set it after you have given all your data to the graph object.
+    attr_accessor :minimum_value
+
+    # You can manually set a maximum value, such as a percentage-based graph that always goes to 100.
+    #
+    # If you use this, you must set it after you have given all your data to the graph object.
+    attr_accessor :maximum_value
+        
+    # Experimental
+    attr_accessor :additional_line_values
+    
+    # Experimental
+    attr_accessor :stacked
+    
+
+    # If one numerical argument is given, the graph is drawn at 4/3 ratio according to the given width (800 results in 800x600, 400 gives 400x300, etc.).
+    #
+    # Or, send a geometry string for other ratios ('800x400', '400x225'). 
+    #
+    # Looks for Bitstream Vera as the default font. Expects an environment var of MAGICK_FONT_PATH to be set. (Uses RMagick's default font otherwise.)
+    def initialize(target_width=800)
+
+      if not Numeric === target_width
+        geometric_width, geometric_height = target_width.split('x')
+        @columns = geometric_width.to_f
+        @rows = geometric_height.to_f
+      else
+        @columns = target_width.to_f
+        @rows = target_width.to_f * 0.75        
+      end
+
+      # Internal for calculations
+      @raw_columns = 800.0
+      @raw_rows = 800.0 * (@rows/@columns)
+      @column_count = 0
+      @maximum_value = @minimum_value = nil
+      @has_data = false
+      @data = Array.new
+      @labels = Hash.new
+      @labels_seen = Hash.new
+      @scale = @columns / @raw_columns
+
+      vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
+      @font = File.exists?(vera_font_path) ? vera_font_path : nil
+
+      @marker_font_size = 21.0
+      @legend_font_size = 20.0
+      @title_font_size = 36.0
+
+      @no_data_message = "No Data"
+
+      @hide_line_markers = @hide_legend = @hide_title = false
+
+      @additional_line_values = []      
+      @additional_line_colors = []
+
+      reset_themes()
+      theme_keynote()
+    end
+    
+    # Add a color to the list of available colors for lines.
+    #
+    # Example: 
+    #  add_color('#c0e9d3')
+    def add_color(colorname)
+      @colors << colorname
+    end
+
+    # Replace the entire color list with a new array of colors. You need to have one more color
+    # than the number of datasets you intend to draw. Also aliased as the colors= setter method.
+    #
+    # Example: 
+    #  replace_colors('#cc99cc', '#d9e043', '#34d8a2')
+    def replace_colors(color_list=[])
+      @colors = color_list
+    end
+
+    # You can set a theme manually. Assign a hash to this method before you send your data.
+    #
+    #  graph.theme = {
+    #    :colors => %w(orange purple green white red),
+    #    :marker_color => 'blue',
+    #    :background_colors => %w(black grey)
+    #  }
+    #
+    # :background_image => 'squirrel.png' is also possible.
+    #
+    # (Or hopefully something better looking than that.)
+    #
+    def theme=(options)
+      defaults = {
+        :colors => ['black', 'white'],
+        :additional_line_colors => ['grey'],
+        :marker_color => 'white',
+        :background_colors => nil,
+        :background_image => nil
+      }
+      options = defaults.merge options
+
+      reset_themes()
+
+      @colors = options[:colors]
+      @marker_color = options[:marker_color]
+      @additional_line_colors = options[:additional_line_colors]
+      if not options[:background_colors].nil?
+        @base_image = render_gradiated_background *options[:background_colors]
+      else
+        @base_image = render_image_background *options[:background_image]
+      end
+    end
+
+    # Add a color to the list of available colors for lines.
+    #
+    # Example: 
+    #  add_color('#c0e9d3')
+    def add_color(colorname)
+      @colors << colorname
+    end
+
+    # Replace the entire color list with a new array of colors. You need to have one more color
+    # than the number of datasets you intend to draw. Also aliased as the colors= setter method.
+    #
+    # Example: 
+    #  replace_colors('#cc99cc', '#d9e043', '#34d8a2')
+    def replace_colors(color_list=[])
+      @colors = color_list
+    end
+    
+    # A color scheme similar to the popular presentation software.
+    def theme_keynote
+      reset_themes()
+      # Colors
+      @blue = '#6886B4'
+      @yellow = '#FDD84E'
+      @green = '#72AE6E'
+      @red = '#D1695E'
+      @purple = '#8A6EAF'
+      @orange = '#EFAA43'
+      @white = 'white'
+      @colors = [@yellow, @blue, @green, @red, @purple, @orange, @white]
+
+      @marker_color = 'white'
+      
+      @base_image = render_gradiated_background('black', '#4a465a')      
+    end
+    
+    # A color scheme plucked from the colors on the popular usability blog.
+    def theme_37signals
+      reset_themes()
+      # Colors
+      @green = '#339933'
+      @purple = '#cc99cc'
+      @blue = '#336699'
+      @yellow = '#FFF804'
+      @red = '#ff0000'
+      @orange = '#cf5910'
+      @black = 'black'
+      @colors = [@yellow, @blue, @green, @red, @purple, @orange, @black]
+
+      @marker_color = 'black'
+      
+      @base_image = render_gradiated_background('#d1edf5', 'white')
+    end
+
+    # A color scheme from the colors used on the 2005 Rails keynote presentation at RubyConf.
+    def theme_rails_keynote
+      reset_themes()
+      # Colors
+      @green = '#00ff00'
+      @grey = '#333333'
+      @orange = '#ff5d00'
+      @red = '#f61100'
+      @white = 'white'
+      @light_grey = '#999999'
+      @black = 'black'
+      @colors = [@green, @grey, @orange, @red, @white, @light_grey, @black]
+
+      @marker_color = 'white'
+      
+      @base_image = render_gradiated_background('#0083a3', '#0083a3')
+    end
+
+    # A color scheme similar to that used on the popular podcast site.
+    def theme_odeo
+      reset_themes()
+      # Colors
+      @grey = '#202020'
+      @white = 'white'
+      @dark_pink = '#a21764'
+      @green = '#8ab438'
+      @light_grey = '#999999'
+      @dark_blue = '#3a5b87'
+      @black = 'black'
+      @colors = [@grey, @white, @dark_blue, @dark_pink, @green, @light_grey, @black]
+
+      @marker_color = 'white'
+      
+      @base_image = render_gradiated_background('#ff47a4', '#ff1f81')
+    end
+
+    # Parameters are an array where the first element is the name of the dataset
+    # and the value is an array of values to plot.
+    #
+    # Can be called multiple times with different datasets for a multi-valued graph.
+    #
+    # If the color argument is nil, the next color from the default theme will be used.
+    #
+    # NOTE: If you want to use a preset theme, you must set it before calling data().
+    #
+    # Example:
+    #
+    #  data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00')
+    #
+    def data(name, data_points=[], color=nil)
+      @data << [name, data_points, color || increment_color]
+      # Set column count if this is larger than previous counts
+      @column_count = (data_points.length > @column_count) ? data_points.length : @column_count
+
+      # Pre-normalize
+      data_points.each_with_index do |data_point, index|
+        next if data_point.nil?
+        
+        # Setup max/min so spread starts at the low end of the data points
+        if @maximum_value.nil? && @minimum_value.nil?
+          @maximum_value = @minimum_value = data_point
+        end
+
+        # TODO Doesn't work with stacked bar graphs
+        #Original: @maximum_value = larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
+        @maximum_value = larger_than_max?(data_point) ? data_point : @maximum_value
+        if @maximum_value > 0
+          @has_data = true
+        end
+        @minimum_value = less_than_min?(data_point) ? data_point : @minimum_value
+        if @minimum_value < 0
+     	  @has_data = true
+        end
+      end
+    end
+
+    # Writes the graph to a file. Defaults to 'graph.png'
+    #
+    # Example: write('graphs/my_pretty_graph.png')
+    def write(filename="graph.png")
+      draw()
+      @base_image.write(filename)
+    end
+
+    # Return the graph as a rendered binary blob.
+    def to_blob(fileformat='PNG')
+      draw()
+      return @base_image.to_blob do
+         self.format = fileformat
+      end
+    end
+
+    def scale_measurements
+      setup_graph_measurements
+    end
+    
+    def total_height
+      @rows + 10
+    end
+    
+    def graph_top
+      @graph_top * @scale
+    end
+    
+    def graph_height
+      @graph_height * @scale
+    end
+    
+    def graph_left 
+      @graph_left * @scale 
+    end
+    
+    def graph_width
+      @graph_width * @scale
+    end
+
+
+protected
+
+
+    def make_stacked
+      stacked_values = Array.new(@column_count, 0)
+      @data.each do |value_set|
+        value_set[1].each_with_index do |value, index|
+          stacked_values[index] += value
+        end
+        value_set[1] = stacked_values.dup
+      end
+    end
+
+
+    # Overridden by subclasses to do the actual plotting of the graph.
+    #
+    # Subclasses should start by calling super() for this method.
+    def draw
+      make_stacked if @stacked
+      setup_drawing()
+  
+      # Subclasses will do some drawing here...
+      #@d.draw(@base_image)
+    end
+
+    # Draws the decorations.
+    # - line markers
+    # - legend
+    # - title
+    def setup_drawing
+      # Maybe should be done in one of the following functions for more granularity.
+      unless @has_data
+        draw_no_data()
+        return
+      end
+      
+      normalize()
+      setup_graph_measurements()
+      sort_norm_data() # Sort norm_data with avg largest values set first (for display)
+      
+      draw_legend()
+      setup_graph_height()
+      draw_line_markers()
+      draw_title
+    end
+
+    # Make copy of data with values scaled between 0-100
+    def normalize
+      if @norm_data.nil?
+        @norm_data = []
+        return unless @has_data
+        @spread = @maximum_value.to_f - @minimum_value.to_f
+        @spread = 20.0 if @spread == 0.0 # Protect from divide by zero
+        min_val = @minimum_value.to_f
+        @data.each do |data_row|
+          norm_data_points = []
+          data_row[DATA_VALUES_INDEX].each do |data_point|
+            if data_point.nil?
+              norm_data_points << nil
+            else
+              norm_data_points << ((data_point.to_f - min_val ) / @spread)
+            end
+          end
+          @norm_data << [data_row[DATA_LABEL_INDEX], norm_data_points, data_row[DATA_COLOR_INDEX]]
+        end
+      end
+    end
+
+    def setup_graph_height
+      @graph_height = @graph_bottom - @graph_top
+    end
+
+    def setup_graph_measurements
+      # TODO Separate horizontal lines from line number labels so they can be shown or hidden independently
+      # TODO Get width of longest left-hand vertical text label and space left margin accordingly
+      unless @hide_line_markers
+        @graph_left = 130.0 # TODO Calculate based on string width of labels
+        @graph_right_margin = 80.0 # TODO see previous line
+        @graph_bottom_margin = 80.0
+      else
+        @graph_left = @graph_right_margin = @graph_bottom_margin =  40
+      end
+
+      @graph_right = @raw_columns - @graph_right_margin
+
+      @graph_width = @raw_columns - @graph_left - @graph_right_margin
+
+      @graph_top = 150.0
+      @graph_bottom = @raw_rows - @graph_bottom_margin
+      setup_graph_height()
+    end
+
+    # Draws horizontal background lines and labels
+    def draw_line_markers
+      return if @hide_line_markers
+            
+      # Draw horizontal line markers and annotate with numbers
+      @d = @d.stroke(@marker_color)
+      @d = @d.stroke_width 1
+      number_of_lines = 4
+
+      # TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
+      spread = @maximum_value.to_f - @minimum_value.to_f
+      spread = spread > 0 ? spread : 1
+      increment = (spread > 0) ? significant(spread / number_of_lines) : 1
+      inc_graph = @graph_height.to_f / (spread / increment)
+
+      (0..number_of_lines).each do |index|
+        y = @graph_top + @graph_height - index.to_f * inc_graph
+        @d = @d.line(@graph_left, y, @graph_right, y)
+
+        marker_label = index * increment + @minimum_value.to_f
+
+        @d.fill = @marker_color
+        @d.font = @font if @font
+        @d.stroke = 'transparent'
+        @d.pointsize = scale_fontsize(@marker_font_size)
+        @d.gravity = EastGravity
+        
+        @d = @d.annotate_scaled( @base_image, 
+                          100, 20,
+                          -10, y - (@marker_font_size/2.0), 
+                          marker_label.to_s, @scale)
+      end
+      i = 0
+      @additional_line_values.each do |value|
+        inc_graph = @graph_height.to_f / (@maximum_value.to_f / value)
+        
+        y = @graph_top + @graph_height - inc_graph
+        
+        @d = @d.stroke(@additional_line_colors[i])
+        @d = @d.line(@graph_left, y, @graph_right, y)
+
+
+        @d.fill = @additional_line_colors[i]
+        @d.font = @font if @font
+        @d.stroke = 'transparent'
+        @d.pointsize = scale_fontsize(@marker_font_size)
+        @d.gravity = EastGravity
+        @d = @d.annotate_scaled( @base_image, 
+                          100, 20,
+                          -10, y - (@marker_font_size/2.0), 
+                          "", @scale)
+        i += 1   
+      end
+    end
+
+    # Draws a legend with the names of the datasets matched to the colors used to draw them.
+    def draw_legend
+      return if @hide_legend
+
+      @legend_labels = @data.collect {|item| item[DATA_LABEL_INDEX] }
+
+      legend_square_width = 20 # small square with color of this item
+
+      # May fix legend drawing problem at small sizes
+      @d.font = @font if @font
+      @d.pointsize = @legend_font_size
+
+      metrics = @d.get_type_metrics(@base_image, @legend_labels.join(''))
+      legend_text_width = metrics.width
+      legend_width = legend_text_width + (@legend_labels.length * legend_square_width * 2.7)
+      legend_left = (@raw_columns - legend_width) / 2
+      legend_increment = legend_width / @legend_labels.length.to_f
+
+      current_x_offset = legend_left
+      @legend_labels.each_with_index do |legend_label, index|        
+        # Draw label
+        @d.fill = @marker_color
+        @d.font = @font if @font
+        @d.pointsize = scale_fontsize(@legend_font_size)
+        @d.stroke = 'transparent'
+        @d.font_weight = NormalWeight
+        @d.gravity = WestGravity
+        @d = @d.annotate_scaled( @base_image, 
+                          @raw_columns, 24,
+                          current_x_offset + (legend_square_width * 1.7), 70, 
+                          legend_label.to_s, @scale)
+        
+        # Now draw box with color of this dataset
+        legend_box_y_offset = 2 # Move box down slightly to center
+        @d = @d.stroke 'transparent'
+        @d = @d.fill @data[index][DATA_COLOR_INDEX]
+        @d = @d.rectangle(current_x_offset, 70 + legend_box_y_offset, 
+                          current_x_offset + legend_square_width, 70 + legend_square_width + legend_box_y_offset)
+
+        @d.pointsize = @legend_font_size
+        metrics = @d.get_type_metrics(@base_image, legend_label.to_s)
+        current_string_offset = metrics.width + (legend_square_width * 2.7)
+        current_x_offset += current_string_offset
+      end
+      @color_index = 0
+    end
+
+    def draw_title
+      return if (@hide_title || @title.nil?)
+      
+      @d.fill = @marker_color
+      @d.font = @font if @font
+      @d.stroke = 'transparent'
+      @d.pointsize = scale_fontsize(@title_font_size)
+      @d.font_weight = BoldWeight
+      @d.gravity = CenterGravity
+      @d = @d.annotate_scaled( @base_image, 
+                        @raw_columns, 50,
+                        0, 10, 
+                        @title, @scale)
+    end
+
+    ##
+    # Draws column labels below graph, centered over x_offset
+    def draw_label(x_offset, index)
+      return if @hide_line_markers
+
+      if !@labels[index].nil? && @labels_seen[index].nil?
+        @d.fill = @marker_color
+        @d.font = @font if @font
+        @d.stroke = 'transparent'
+        @d.font_weight = NormalWeight
+        @d.pointsize = scale_fontsize(@marker_font_size)
+        @d.gravity = CenterGravity
+        @d = @d.annotate_scaled(@base_image,
+                                1, 1,
+                                x_offset, @raw_rows - (@graph_bottom_margin - 30),
+                                @labels[index], @scale)
+        @labels_seen[index] = 1
+      end
+    end
+
+    def draw_no_data
+        @d.fill = @marker_color
+        @d.font = @font if @font
+        @d.stroke = 'transparent'
+        @d.font_weight = NormalWeight
+        @d.pointsize = scale_fontsize(80)
+        @d.gravity = CenterGravity
+        @d = @d.annotate_scaled( @base_image, 
+                        @raw_columns, @raw_rows/2.0,
+                        0, 10, 
+                        @no_data_message, @scale)
+    end
+
+    # Use with a theme definition method to draw a gradiated (or solid color) background.
+    def render_gradiated_background(top_color, bottom_color)
+      Image.new(@columns, @rows, 
+          GradientFill.new(0, 0, 100, 0, top_color, bottom_color))
+    end
+
+    # Use with a theme to use an image (800x600 original) background.
+    def render_image_background(image_path)
+      image = Image.read(image_path)
+      if @scale != 1.0
+        image[0].resize!(@scale) # TODO Resize with new scale (crop if necessary for wide graph)
+      end
+      image[0]
+    end
+    
+    # Use with a theme to make a transparent background
+    def render_transparent_background
+      Image.new(@columns, @rows) do
+        self.background_color = 'transparent'
+      end
+    end
+
+    def reset_themes
+      @color_index = 0
+      @labels_seen = Hash.new
+      
+      @d = Draw.new
+      # Scale down from 800x600 used to calculate drawing.
+      # NOTE: Font annotation is now affected and has to be done manually.
+      @d = @d.scale(@scale, @scale)
+    end
+
+    def scale(value)
+      value * @scale
+    end
+    
+    def scale_fontsize(value)
+      new_fontsize = value * @scale
+      #return 10 if new_fontsize < 10
+      return new_fontsize
+    end
+
+    def clip_value_if_greater_than(value, max_value)
+      (value > max_value) ? max_value : value
+    end
+
+    # Overridden by subclasses such as stacked bar.
+    def larger_than_max?(data_point, index=0)
+      data_point > @maximum_value
+    end
+
+	  def less_than_min?(data_point, index=0)
+      data_point < @minimum_value
+    end
+
+    def max(data_point, index)
+      data_point
+    end
+
+	  def min(data_point, index)
+      data_point
+    end
+   
+    def significant(inc)
+      return 1.0 if inc == 0 # Keep from going into infinite loop
+      factor = 1.0
+      while (inc < 10)
+        inc *= 10
+        factor /= 10
+      end
+
+      while (inc > 100)
+        inc /= 10
+        factor *= 10
+      end
+
+      res = inc.floor * factor
+      if (res.to_i.to_f == res)
+        res.to_i
+      else
+        res
+      end
+    end
+
+    # Sort with largest overall summed value at front of array 
+    # so it shows up correctly in the drawn graph.
+    def sort_norm_data
+      @norm_data.sort! { |a,b| sums(b[1]) <=> sums(a[1]) }
+    end
+
+    def sums(data_set)
+      total_sum = 0
+      data_set.collect {|num| total_sum += num.to_f }
+      total_sum
+    end
+
+    def get_maximum_by_stack
+      # Get sum of each stack
+      max_hash = {}
+      @data.each do |data_set|
+        data_set[DATA_VALUES_INDEX].each_with_index do |data_point, i|
+          max_hash[i] = 0.0 unless max_hash[i]
+          max_hash[i] += data_point.to_f
+        end
+      end
+
+      @maximum_value = 0
+      max_hash.keys.each do |key|
+        @maximum_value = max_hash[key] if max_hash[key] > @maximum_value
+      end
+      @minimum_value = 0
+    end
+
+
+private
+    
+    def increment_color
+      if @color_index == 0
+        @color_index += 1
+        return @colors[0]
+      else
+        if @color_index < @colors.length
+          @color_index += 1
+          return @colors[@color_index - 1]
+        else
+          # Start over
+          @color_index = 0
+          return @colors[-1]
+          #raise(ColorlistExhaustedException, "There are no more colors left to use.")
+        end
+      end
+    end
+
+  end
+  
+  class ColorlistExhaustedException < StandardError; end
+      
+end
+
+
+module Magick
+  class Draw
+    
+    # Additional method since Draw.scale doesn't affect annotations.
+    def annotate_scaled(img, width, height, x, y, text, scale)
+      scaled_width = (width * scale) >= 1 ? (width * scale) : 1
+      scaled_height = (height * scale) >= 1 ? (height * scale) : 1
+      
+      self.annotate( img, 
+                      scaled_width, scaled_height,
+                      x * scale, y * scale,
+                      text)
+    end
+    
+  end
+end
diff --git a/vendor/plugins/gruff/lib/gruff/line.rb b/vendor/plugins/gruff/lib/gruff/line.rb
new file mode 100644
index 0000000..928b927
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff/line.rb
@@ -0,0 +1,95 @@
+
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::Line < Gruff::Base
+
+  # Draw a dashed line at the given value
+  attr_accessor :baseline_value
+	
+  # Color of the baseline
+  attr_accessor :baseline_color
+  
+  # Hide parts of the graph to fit more datapoints, or for a different appearance.
+  attr_accessor :hide_dots, :hide_lines
+
+  # Call with target pixel width of graph (800, 400, 300), and/or 'false' to omit lines (points only).
+  #
+  #  g = Gruff::Line.new(400) # 400px wide with lines
+  #
+  #  g = Gruff::Line.new(400, false) # 400px wide, no lines (for backwards compatibility)
+  #
+  #  g = Gruff::Line.new(false) # Defaults to 800px wide, no lines (for backwards compatibility)
+  # 
+  # The preferred way is to call hide_dots or hide_lines instead.
+  def initialize(*args)
+    raise ArgumentError, "Wrong number of arguments" if args.length > 2
+    if args.empty? or ((not Numeric === args.first) && (not String === args.first)) then
+      super()
+    else
+      super args.shift
+    end
+    
+    @hide_dots = @hide_lines = false
+    @baseline_color = 'red'
+  end
+
+  def draw
+    super
+
+    return unless @has_data
+      
+    @x_increment = @graph_width / (@column_count - 1).to_f
+    circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
+
+    @d = @d.stroke_opacity 1.0
+    @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
+ 
+    if (defined?(@norm_baseline)) then
+      level = @graph_top + (@graph_height - @norm_baseline * @graph_height)
+      @d = @d.push
+      @d.stroke_color @baseline_color
+      @d.fill_opacity 0.0
+      @d.stroke_dasharray(10, 20)
+      @d.stroke_width 5
+      @d.line(@graph_left, level, @graph_left + @graph_width, level)
+      @d = @d.pop
+    end
+
+    @norm_data.each do |data_row|
+      prev_x = prev_y = nil
+      @d = @d.stroke data_row[DATA_COLOR_INDEX]
+      @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+      data_row[1].each_with_index do |data_point, index|
+        new_x = @graph_left + (@x_increment * index)
+        next if data_point.nil?
+
+        draw_label(new_x, index)
+
+        new_y = @graph_top + (@graph_height - data_point * @graph_height)
+
+        if !@hide_lines and !prev_x.nil? and !prev_y.nil? then
+          @d = @d.line(prev_x, prev_y, new_x, new_y)
+        end
+        @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y) unless @hide_dots
+
+        prev_x = new_x
+        prev_y = new_y
+      end
+
+    end
+
+    @d.draw(@base_image)
+  end
+
+  def normalize
+    @maximum_value = max(@maximum_value.to_f, @baseline_value.to_f)
+    super
+    @norm_baseline = (@baseline_value.to_f / @maximum_value.to_f) if @baseline_value
+  end
+  
+  def max(a, b)
+    (a < b ? b : a)
+  end
+
+end
diff --git a/vendor/plugins/gruff/lib/gruff/net.rb b/vendor/plugins/gruff/lib/gruff/net.rb
new file mode 100644
index 0000000..8187a0c
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff/net.rb
@@ -0,0 +1,133 @@
+
+require File.dirname(__FILE__) + '/base'
+
+# Experimental!!! See also the Spider graph.
+class Gruff::Net < Gruff::Base
+
+  def draw
+
+    super
+
+    return unless @has_data
+
+    @radius = @graph_height / 2.0
+    @center_x = @graph_left + (@graph_width / 2.0)
+    @center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
+
+    @x_increment = @graph_width / (@column_count - 1).to_f
+    circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
+
+    @d = @d.stroke_opacity 1.0
+    @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
+
+    if (defined?(@norm_baseline)) then
+      level = @graph_top + (@graph_height - @norm_baseline * @graph_height)
+      @d = @d.push
+      @d.stroke_color @baseline_color
+      @d.fill_opacity 0.0
+      @d.stroke_dasharray(10, 20)
+      @d.stroke_width 5
+      @d.line(@graph_left, level, @graph_left + @graph_width, level)
+      @d = @d.pop
+    end
+
+    @norm_data.each do |data_row|
+      prev_x = prev_y = nil
+      @d = @d.stroke data_row[DATA_COLOR_INDEX]
+      @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+      data_row[1].each_with_index do |data_point, index|
+        next if data_point.nil?
+
+        rad_pos = index * Math::PI * 2 / @column_count
+        point_distance = data_point * @radius
+        start_x = @center_x + Math::sin(rad_pos) * point_distance
+        start_y = @center_y - Math::cos(rad_pos) * point_distance
+
+        next_index = index + 1 < data_row[1].length ? index + 1 : 0
+
+        next_rad_pos = next_index * Math::PI * 2 / @column_count
+        next_point_distance = data_row[1][next_index] * @radius
+        end_x = @center_x + Math::sin(next_rad_pos) * next_point_distance
+        end_y = @center_y - Math::cos(next_rad_pos) * next_point_distance
+
+        @d = @d.line(start_x, start_y, end_x, end_y)
+
+        @d = @d.circle(start_x, start_y, start_x - circle_radius, start_y) unless @hide_dots
+      end
+
+    end
+
+    @d.draw(@base_image)
+  end
+
+
+  # the lines connecting in the center, with the first line vertical
+  def draw_line_markers
+    return if @hide_line_markers
+
+
+    # have to do this here (AGAIN)... see draw() in this class
+    # because this funtion is called before the @radius, @center_x and @center_y are set
+    @radius = @graph_height / 2.0
+    @center_x = @graph_left + (@graph_width / 2.0)
+    @center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
+
+
+    # Draw horizontal line markers and annotate with numbers
+    @d = @d.stroke(@marker_color)
+    @d = @d.stroke_width 1
+
+
+    (0..@column_count-1).each do |index|
+      rad_pos = index * Math::PI * 2 / @column_count
+
+      @d = @d.line(@center_x, @center_y, @center_x + Math::sin(rad_pos) * @radius, @center_y - Math::cos(rad_pos) * @radius)
+
+
+      marker_label = labels[index] ? labels[index].to_s : '000'
+
+      draw_label(@center_x, @center_y, rad_pos * 360 / (2 * Math::PI), @radius, marker_label)
+    end
+  end
+
+private
+
+  def draw_label(center_x, center_y, angle, radius, amount)
+    r_offset = 1.1
+    x_offset = center_x # + 15 # The label points need to be tweaked slightly
+    y_offset = center_y # + 0  # This one doesn't though
+    x = x_offset + (radius * r_offset * Math.sin(angle.deg2rad))
+    y = y_offset - (radius * r_offset * Math.cos(angle.deg2rad))
+
+    # Draw label
+    @d.fill = @marker_color
+    @d.font = @font if @font
+    @d.pointsize = scale_fontsize(20)
+    @d.stroke = 'transparent'
+    @d.font_weight = BoldWeight
+    s = angle.deg2rad / (2*Math::PI)
+    @d.gravity = SouthGravity     if s >= 0.96 or s < 0.04
+    @d.gravity = SouthWestGravity if s >= 0.04 or s < 0.21
+    @d.gravity = WestGravity      if s >= 0.21 or s < 0.29
+    @d.gravity = NorthWestGravity if s >= 0.29 or s < 0.46
+    @d.gravity = NorthGravity     if s >= 0.46 or s < 0.54
+    @d.gravity = NorthEastGravity if s >= 0.54 or s < 0.71
+    @d.gravity = EastGravity      if s >= 0.71 or s < 0.79
+    @d.gravity = SouthEastGravity if s >= 0.79 or s < 0.96
+#     @d.gravity = NorthGravity
+    @d.annotate_scaled(@base_image, 0, 0, x, y, amount, @scale)
+  end
+
+end
+
+
+class Float
+  # Used for degree => radian conversions
+  def deg2rad
+    self * (Math::PI/180.0)
+  end
+end
+
+
+
diff --git a/vendor/plugins/gruff/lib/gruff/photo_bar.rb b/vendor/plugins/gruff/lib/gruff/photo_bar.rb
new file mode 100644
index 0000000..e128db1
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff/photo_bar.rb
@@ -0,0 +1,100 @@
+require File.dirname(__FILE__) + '/base'
+
+# EXPERIMENTAL!
+#
+# Doesn't work yet.
+#
+class Gruff::PhotoBar < Gruff::Base
+
+# TODO
+#
+# define base and cap in yml
+# allow for image directory to be located elsewhere
+# more exact measurements for bar heights (go all the way to the bottom of the graph)
+# option to tile images instead of use a single image
+# drop base label a few px lower so photo bar graphs can have a base dropping over the lower marker line
+#
+
+  # The name of a pre-packaged photo-based theme.
+  attr_accessor :theme
+
+#   def initialize(target_width=800)
+#     super
+#     init_photo_bar_graphics()
+#   end
+
+  def draw
+    super
+    return unless @has_data
+
+    return # TODO Remove for further development
+
+    init_photo_bar_graphics()
+    
+    #Draw#define_clip_path()
+    #Draw#clip_path(pathname)
+    #Draw#composite....with bar graph image OverCompositeOp
+    #
+    # See also
+    #
+    # Draw.pattern # define an image to tile as the filling of a draw object
+    # 
+
+    # Setup spacing.
+    #
+    # Columns sit side-by-side.
+    spacing_factor = 0.9
+    @bar_width = @norm_data[0][DATA_COLOR_INDEX].columns
+
+    @norm_data.each_with_index do |data_row, row_index|
+  
+      data_row[1].each_with_index do |data_point, point_index|
+        data_point = 0 if data_point.nil?
+        # Use incremented x and scaled y
+        left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index)))
+        left_y = @graph_top + (@graph_height - data_point * @graph_height) + 1
+        right_x = left_x + @bar_width * spacing_factor
+        right_y = @graph_top + @graph_height - 1
+      
+        bar_image_width = data_row[DATA_COLOR_INDEX].columns
+        bar_image_height = right_y.to_f - left_y.to_f
+      
+        # Crop to scale for data
+        bar_image = data_row[DATA_COLOR_INDEX].crop(0, 0, bar_image_width, bar_image_height)
+        
+        @d.gravity = NorthWestGravity
+        @d = @d.composite(left_x, left_y, bar_image_width, bar_image_height, bar_image)
+      
+        # Calculate center based on bar_width and current row
+        label_center = @graph_left + (@data.length * @bar_width * point_index) + (@data.length * @bar_width / 2.0)
+        draw_label(label_center, point_index)
+      end
+
+    end
+
+    @d.draw(@base_image)    
+  end
+
+
+  # Return the chosen theme or the default
+  def theme
+    @theme || 'plastik'
+  end
+
+protected
+
+  # Sets up colors with a list of images that will be used.
+  # Images should be 340px tall
+  def init_photo_bar_graphics    
+    color_list = Array.new
+    theme_dir = File.dirname(__FILE__) + '/../../assets/' + theme
+
+    Dir.open(theme_dir).each do |file|
+      next unless /\.png$/.match(file)
+      color_list << Image.read("#{theme_dir}/#{file}").first
+    end
+    @colors = color_list
+  end
+
+end
+
diff --git a/vendor/plugins/gruff/lib/gruff/pie.rb b/vendor/plugins/gruff/lib/gruff/pie.rb
new file mode 100644
index 0000000..0b85e34
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff/pie.rb
@@ -0,0 +1,88 @@
+
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::Pie < Gruff::Base
+
+  def draw
+    @hide_line_markers = true
+    
+    super
+
+    return unless @has_data
+
+    diameter = @graph_height
+    radius = [@graph_width, @graph_height].min / 2.0 
+    top_x = @graph_left + (@graph_width - diameter) / 2.0
+    center_x = @graph_left + (@graph_width / 2.0)
+    center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
+    total_sum = sums_for_pie()
+    prev_degrees = 0.0
+
+    # Use full data since we can easily calculate percentages
+    @data.each do |data_row|
+      if data_row[1][0] > 0
+        @d = @d.stroke data_row[DATA_COLOR_INDEX]
+        @d = @d.fill 'transparent'
+        @d.stroke_width(radius) # stroke width should be equal to radius. we'll draw centered on (radius / 2)
+
+        current_degrees = (data_row[1][0] / total_sum) * 360.0 
+
+        # ellipse will draw the the stroke centered on the first two parameters offset by the second two.
+        # therefore, in order to draw a circle of the proper diameter we must center the stroke at
+        # half the radius for both x and y
+        @d = @d.ellipse(center_x, center_y, 
+                  radius / 2.0, radius / 2.0,
+                  prev_degrees, prev_degrees + current_degrees + 0.5) # <= +0.5 'fudge factor' gets rid of the ugly gaps
+                  
+        half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2
+      
+
+        @d = draw_label(center_x,center_y, 
+                    half_angle, 
+                    radius, 
+                    ((data_row[1][0] / total_sum) * 100).round.to_s + '%     ')
+      
+        prev_degrees += current_degrees
+      end
+    end
+
+    @d.draw(@base_image)
+  end
+
+private
+
+  def draw_label(center_x, center_y, angle, radius, amount)
+    r_offset = 30      # The distance out from the center of the pie to get point
+    x_offset = center_x + 15 # The label points need to be tweaked slightly
+    y_offset = center_y + 0  # This one doesn't though
+    x = x_offset + ((radius + r_offset) * Math.cos(angle.deg2rad))
+    y = y_offset + ((radius + r_offset) * Math.sin(angle.deg2rad))
+    
+    # Draw label
+    @d.fill = @marker_color
+    @d.font = @font if @font
+    @d.pointsize = scale_fontsize(20)
+    @d.stroke = 'transparent'
+    @d.font_weight = BoldWeight
+    @d.gravity = CenterGravity
+    @d.annotate_scaled( @base_image, 
+                      0, 0,
+                      x, y, 
+                      amount, @scale)
+  end
+
+  def sums_for_pie
+    total_sum = 0.0
+    @data.collect {|data_row| total_sum += data_row[1][0] }
+    total_sum
+  end
+
+end
+
+
+class Float
+  # Used for degree => radian conversions
+  def deg2rad
+    self * (Math::PI/180.0)
+  end
+end
diff --git a/vendor/plugins/gruff/lib/gruff/scene.rb b/vendor/plugins/gruff/lib/gruff/scene.rb
new file mode 100644
index 0000000..a787a15
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff/scene.rb
@@ -0,0 +1,196 @@
+
+require "observer"
+require File.dirname(__FILE__) + '/base'
+
+# EXPERIMENTAL!
+#
+# Started by Geoffrey Grosenbach at Canada on Rails, April 2006.
+#
+# A scene is a non-linear graph that assembles layers together to tell a story.
+# Layers are folders with appropriately named files (see below). You can group 
+# layers and control them together or just set their values individually.
+#
+# Examples:
+#
+# * A city scene that changes with the time of day and the weather conditions.
+# * A traffic map that shows red lines on streets that are crowded and green on free-flowing ones.
+#
+# Usage:
+# 
+#  g = Gruff::Scene.new("500x100", "artwork/city_scene")
+#  g.layers = %w(background haze sky clouds)
+#  g.weather_group = %w(clouds)
+#  g.time_group = %w(background sky)
+#  g.weather = "cloudy"
+#  g.time = Time.now
+#  g.haze = true
+#  g.write "hazy_daytime_city_scene.png"
+#
+#
+#
+# If there is a file named 'default.png', it will be selected (unless other values are provided to override it).
+#
+class Gruff::Scene < Gruff::Base
+    
+  # An array listing the foldernames that will be rendered, from back to front.
+  #
+  #  g.layers = %w(sky clouds buildings street people)
+  #
+  attr_accessor :layers
+
+  def initialize(target_width, base_dir)
+    @base_dir = base_dir
+    @groups = {}
+    @layers = []    
+    super target_width
+  end
+
+  def draw
+    # Join all the custom paths and filter out the empty ones
+    image_paths = @layers.map { |layer| layer.path }.select { |path| !path.empty? }
+    images = Magick::ImageList.new(*image_paths)
+    @base_image = images.flatten_images
+  end
+
+  def layers=(ordered_list)
+    ordered_list.each do |layer_name|
+      @layers << Gruff::Layer.new(@base_dir, layer_name)
+    end
+  end
+
+  # Group layers to input values
+  #
+  #  g.weather_group = ["sky", "sea", "clouds"]
+  #
+  # Set input values
+  #
+  #  g.weather = "cloudy"
+  #
+  def method_missing(method_name, *args)
+    case method_name.to_s
+    when /^(\w+)_group=$/
+      add_group $1, *args
+      return
+    when /^(\w+)=$/
+      set_input $1, args.first
+      return
+    end
+    super
+  end
+
+private
+
+  def add_group(input_name, layer_names)
+    @groups[input_name] = Gruff::Group.new(input_name, @layers.select { |layer| layer_names.include?(layer.name) })
+  end
+
+  def set_input(input_name, input_value)
+    if not @groups[input_name].nil?
+      @groups[input_name].send_updates(input_value)
+    else
+      if chosen_layer = @layers.detect { |layer| layer.name == input_name }
+        chosen_layer.update input_value
+      end
+    end
+  end
+  
+end
+
+
+class Gruff::Group
+
+  include Observable
+  attr_reader :name
+
+  def initialize(folder_name, layers)
+    @name = folder_name
+    layers.each do |layer|
+      layer.observe self
+    end
+  end
+  
+  def send_updates(value)
+    changed
+    notify_observers value
+  end
+  
+end
+
+
+class Gruff::Layer
+  
+  attr_reader :name
+  
+  def initialize(base_dir, folder_name)
+    @base_dir = base_dir.to_s
+    @name = folder_name.to_s
+    @filenames = Dir.open(File.join(base_dir, folder_name)).entries.select { |file| file =~ /^[^.]+\.png$/ }
+    @selected_filename = select_default
+  end
+  
+  # Register this layer so it receives updates from the group
+  def observe(obj)
+    obj.add_observer self
+  end
+  
+  # Choose the appropriate filename for this layer, based on the input
+  def update(value)
+    @selected_filename =  case value.to_s
+                          when /^(true|false)$/
+                            select_boolean value
+                          when /^(\w|\s)+$/
+                            select_string value
+                          when /^-?(\d+\.)?\d+$/
+                            select_numeric value
+                          when /(\d\d):(\d\d):\d\d/
+                            select_time "#{$1}#{$2}"
+                          else
+                            select_default
+                          end
+  end
+
+  # Returns the full path to the selected image, or a blank string
+  def path
+    unless @selected_filename.nil? || @selected_filename.empty?
+      return File.join(@base_dir, @name, @selected_filename)
+    end
+    ''
+  end
+
+private
+
+  # Match "true.png" or "false.png"
+  def select_boolean(value)
+    file_exists_or_blank value.to_s
+  end
+
+  # Match -5 to _5.png
+  def select_numeric(value)
+    file_exists_or_blank value.to_s.gsub('-', '_')
+  end
+  
+  def select_time(value)
+    times = @filenames.map { |filename| filename.gsub('.png', '') }
+    times.each_with_index do |time, index|
+      if (time > value) && (index > 0)
+        return "#{times[index - 1]}.png"
+      end
+    end
+    return "#{times.last}.png"
+  end
+  
+  # Match "partly cloudy" to "partly_cloudy.png"
+  def select_string(value)
+    file_exists_or_blank value.to_s.gsub(' ', '_')
+  end
+  
+  def select_default
+    file_exists_or_blank "default"
+  end
+
+  # Returns the string "#{filename}.png", if it exists
+  def file_exists_or_blank(filename)
+    @filenames.include?("#{filename}.png") ? "#{filename}.png" : ''
+  end
+  
+end
diff --git a/vendor/plugins/gruff/lib/gruff/side_stacked_bar.rb b/vendor/plugins/gruff/lib/gruff/side_stacked_bar.rb
new file mode 100644
index 0000000..b927719
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff/side_stacked_bar.rb
@@ -0,0 +1,119 @@
+# New gruff graph type added to enable sideways stacking bar charts (basically looks like a x/y
+# flip of a standard stacking bar chart)
+#
+# alun.eyre@googlemail.com 
+#
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::SideStackedBar < Gruff::Base
+
+    # instead of base class version, draws vertical background lines and label
+    def draw_line_markers
+
+      return if @hide_line_markers
+
+      # Draw horizontal line markers and annotate with numbers
+      @d = @d.stroke(@marker_color)
+      @d = @d.stroke_width 1
+      number_of_lines = 5
+
+      # TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
+      increment = significant(@maximum_value.to_f / number_of_lines)
+      (0..number_of_lines).each do |index|
+
+        line_diff = (@graph_right - @graph_left) / number_of_lines
+        x = @graph_right - (line_diff * index) - 1
+        @d = @d.line(x, @graph_bottom, x, @graph_top)
+
+        diff = index - number_of_lines
+        marker_label = diff.abs * increment
+
+        @d.fill = @marker_color
+        @d.font = @font if @font
+        @d.stroke = 'transparent'
+        @d.pointsize = scale_fontsize(@marker_font_size)
+#        @d.gravity = NorthGravity
+        @d = @d.annotate_scaled( @base_image, 
+                          100, 20,
+                          x - (@marker_font_size/1.5), @graph_bottom + 40, 
+                          marker_label.to_s, @scale)
+
+      end
+    end
+
+    # instead of base class version, modified to enable us to draw on the Y axis instead of X
+    def draw_label(y_offset, index)
+      if !@labels[index].nil? && @labels_seen[index].nil?
+        @d.fill = @marker_color
+        @d.font = @font if @font
+        @d.stroke = 'transparent'
+        @d.font_weight = NormalWeight
+        @d.pointsize = scale_fontsize(@marker_font_size)
+        @d.gravity = CenterGravity
+        @d = @d.annotate_scaled(@base_image,
+                                1, 1,
+                                @graph_left / 2, y_offset,
+                                @labels[index], @scale)
+        @labels_seen[index] = 1
+      end
+    end
+
+    def draw
+      get_maximum_by_stack
+      super
+
+      return unless @has_data
+
+      # Setup spacing.
+      #
+      # Columns sit stacked.
+      spacing_factor = 0.9
+
+      @bar_width = @graph_height / @column_count.to_f
+      @d = @d.stroke_opacity 0.0
+      height = Array.new(@column_count, 0)
+      length = Array.new(@column_count, @graph_left)
+
+      @norm_data.each_with_index do |data_row, row_index|
+        @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+        data_row[1].each_with_index do |data_point, point_index|
+
+      	  ## using the original calcs from the stacked bar chart to get the difference between
+      	  ## part of the bart chart we wish to stack.
+      	  temp1 = @graph_left + (@graph_width -
+                                      data_point * @graph_width - 
+                                      height[point_index]) + 1
+      	  temp2 = @graph_left + @graph_width - height[point_index] - 1
+      	  difference = temp2 - temp1
+
+      	  left_x = length[point_index] #+ 1
+                left_y = @graph_top + (@bar_width * point_index)
+      	  right_x = left_x + difference
+                right_y = left_y + @bar_width * spacing_factor
+      	  length[point_index] += difference
+          height[point_index] += (data_point * @graph_width - 2)
+
+          @d = @d.rectangle(left_x, left_y, right_x, right_y)
+
+          # Calculate center based on bar_width and current row
+          label_center = @graph_top + (@bar_width * point_index) + (@bar_width * spacing_factor / 2.0)
+          draw_label(label_center, point_index)
+        end
+
+      end
+
+      @d.draw(@base_image)    
+    end
+
+    protected
+
+    def larger_than_max?(data_point, index=0)
+      max(data_point, index) > @maximum_value
+    end
+
+    def max(data_point, index)
+      @data.inject(0) {|sum, item| sum + item[1][index]}
+    end
+
+end
diff --git a/vendor/plugins/gruff/lib/gruff/spider.rb b/vendor/plugins/gruff/lib/gruff/spider.rb
new file mode 100644
index 0000000..8e9f191
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff/spider.rb
@@ -0,0 +1,130 @@
+
+require File.dirname(__FILE__) + '/base'
+
+# Experimental!!! See also the Net graph.
+#
+# Submitted by Kevin Clark http://glu.ttono.us/
+class Gruff::Spider < Gruff::Base
+  
+  # Hide all text
+  attr_accessor :hide_text
+  attr_accessor :hide_axes
+  attr_accessor :transparent_background
+  
+  def transparent_background=(value)
+    @transparent_background = value
+    @base_image = render_transparent_background if value
+  end
+
+  def hide_text=(value)
+    @hide_title = @hide_text = value
+  end
+  
+  def initialize(max_value, target_width = 800)
+    super(target_width)
+    @max_value = max_value
+    @hide_legend = true;
+  end
+  
+  def draw
+    @hide_line_markers = true
+    
+    super
+
+    return unless @has_data
+
+    # Setup basic positioning
+    diameter = @graph_height
+    radius = @graph_height / 2.0
+    top_x = @graph_left + (@graph_width - diameter) / 2.0
+    center_x = @graph_left + (@graph_width / 2.0)
+    center_y = @graph_top + (@graph_height / 2.0) - 25 # Move graph up a bit
+    
+    @unit_length = radius / @max_value
+    
+        
+    total_sum = sums_for_spider
+    prev_degrees = 0.0
+    additive_angle = (2 * Math::PI)/ @data.size
+    
+    current_angle = 0.0
+
+    # Draw axes
+    draw_axes(center_x, center_y, radius, additive_angle) unless hide_axes    
+    
+    # Draw polygon
+    draw_polygon(center_x, center_y, additive_angle)
+    
+     
+    @d.draw(@base_image)
+  end
+
+private
+  
+  def normalize_points(value)
+    value * @unit_length
+  end
+  
+  def draw_label(center_x, center_y, angle, radius, amount)
+    r_offset = 50      # The distance out from the center of the pie to get point
+    x_offset = center_x      # The label points need to be tweaked slightly
+    y_offset = center_y + 0  # This one doesn't though
+    x = x_offset + ((radius + r_offset) * Math.cos(angle))
+    y = y_offset + ((radius + r_offset) * Math.sin(angle))
+    
+    # Draw label
+    @d.fill = @marker_color
+    @d.font = @font if @font
+    @d.pointsize = scale_fontsize(legend_font_size)
+    @d.stroke = 'transparent'
+    @d.font_weight = BoldWeight
+    @d.gravity = CenterGravity
+    @d.annotate_scaled( @base_image, 
+                      0, 0,
+                      x, y, 
+                      amount, @scale)
+  end
+  
+  def draw_axes(center_x, center_y, radius, additive_angle, line_color = nil)
+    return if hide_axes
+    
+    current_angle = 0.0
+    
+    @data.each do |data_row|
+      @d.stroke(line_color || data_row[DATA_COLOR_INDEX])
+      @d.stroke_width 5.0
+    
+      x_offset = radius * Math.cos(current_angle)
+      y_offset = radius * Math.sin(current_angle)
+
+      @d.line(center_x, center_y,
+              center_x + x_offset,
+              center_y + y_offset)
+            
+      draw_label(center_x, center_y, current_angle, radius, data_row[0].to_s) unless hide_text
+            
+      current_angle += additive_angle
+    end
+  end
+  
+  def draw_polygon(center_x, center_y, additive_angle, color = nil)
+    points = []
+    current_angle = 0.0
+    @data.each do |data_row|
+      points << center_x + normalize_points(data_row[1][0]) * Math.cos(current_angle)
+      points << center_y + normalize_points(data_row[1][0]) * Math.sin(current_angle)
+      current_angle += additive_angle
+    end
+    
+    @d.stroke_width 1.0
+    @d.stroke(color || @marker_color)
+    @d.fill(color || @marker_color)
+    @d.fill_opacity 0.4
+    @d.polygon(*points)
+  end
+  
+  def sums_for_spider
+    @data.inject(0.0) {|sum, data_row| sum += data_row[1][0]}
+  end
+
+end
diff --git a/vendor/plugins/gruff/lib/gruff/stacked_bar.rb b/vendor/plugins/gruff/lib/gruff/stacked_bar.rb
new file mode 100644
index 0000000..bf28df7
--- /dev/null
+++ b/vendor/plugins/gruff/lib/gruff/stacked_bar.rb
@@ -0,0 +1,49 @@
+
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::StackedBar < Gruff::Base
+
+    # Draws a bar graph, but multiple sets are stacked on top of each other.
+    def draw
+      get_maximum_by_stack
+      super
+      return unless @has_data
+
+      # Setup spacing.
+      #
+      # Columns sit stacked.
+      spacing_factor = 0.9
+      @bar_width = @graph_width / @column_count.to_f
+    
+      @d = @d.stroke_opacity 0.0
+      
+      height = Array.new(@column_count, 0)
+    
+      @norm_data.each_with_index do |data_row, row_index|
+        @d = @d.fill data_row[DATA_COLOR_INDEX]
+      
+        data_row[1].each_with_index do |data_point, point_index|
+          # Use incremented x and scaled y
+          left_x = @graph_left + (@bar_width * point_index)
+          left_y = @graph_top + (@graph_height -
+                                 data_point * @graph_height - 
+                                 height[point_index]) + 1
+          right_x = left_x + @bar_width * spacing_factor
+          right_y = @graph_top + @graph_height - height[point_index] - 1
+          
+          # update the total height of the current stacked bar
+          height[point_index] += (data_point * @graph_height - 2)
+          
+          @d = @d.rectangle(left_x, left_y, right_x, right_y)
+          
+          # Calculate center based on bar_width and current row
+          label_center = @graph_left + (@bar_width * point_index) + (@bar_width * spacing_factor / 2.0)
+          draw_label(label_center, point_index)
+        end
+
+      end
+    
+      @d.draw(@base_image)    
+    end
+
+end
-- 
GitLab