Permalink
Browse files

First import from Subversion

  • Loading branch information...
0 parents commit b5788a43b0d9714ed2e53dfa138d41c22ea4649c @topfunky committed Mar 11, 2008
Showing with 5,282 additions and 0 deletions.
  1. +2 −0 .gitignore
  2. +89 −0 CHANGELOG
  3. +21 −0 MIT-LICENSE
  4. +72 −0 Manifest.txt
  5. +15 −0 README.txt
  6. +52 −0 Rakefile
  7. BIN artwork/city_scene.psd
  8. 0 artwork/graphs/Backup of Untitled.key/.typeAttributes.dict
  9. +1 −0 artwork/graphs/Backup of Untitled.key/Contents/PkgInfo
  10. BIN artwork/graphs/Backup of Untitled.key/index.apxl.gz
  11. BIN artwork/graphs/Backup of Untitled.key/thumbs/st0.tiff
  12. BIN artwork/graphs/Backup of Untitled.key/thumbs/st1.tiff
  13. BIN artwork/graphs/Backup of Untitled.key/thumbs/st2.tiff
  14. BIN artwork/graphs/Backup of Untitled.key/thumbs/st3.tiff
  15. 0 artwork/graphs/Untitled.key/.typeAttributes.dict
  16. +1 −0 artwork/graphs/Untitled.key/Contents/PkgInfo
  17. BIN artwork/graphs/Untitled.key/index.apxl.gz
  18. BIN artwork/graphs/Untitled.key/thumbs/st0.tiff
  19. BIN artwork/graphs/Untitled.key/thumbs/st1.tiff
  20. BIN artwork/graphs/Untitled.key/thumbs/st2.tiff
  21. BIN artwork/graphs/Untitled.key/thumbs/st3.tiff
  22. BIN artwork/graphs/samples.001.png
  23. BIN artwork/graphs/samples.002.png
  24. BIN artwork/graphs/samples.003.png
  25. BIN artwork/graphs/samples.004.png
  26. +36 −0 artwork/gruff_controller.rb
  27. +32 −0 artwork/gruffs_helper.rb
  28. +15 −0 artwork/install_rmagick.sh
  29. BIN artwork/mrplot-0.0.1.tar.gz
  30. BIN artwork/plastik-blue.psd
  31. BIN artwork/plastik-green.psd
  32. BIN artwork/plastik-red.psd
  33. BIN artwork/plastik-template.psd
  34. BIN artwork/plastik.png
  35. BIN assets/bubble.png
  36. BIN assets/city_scene/background/0000.png
  37. BIN assets/city_scene/background/0600.png
  38. BIN assets/city_scene/background/2000.png
  39. BIN assets/city_scene/clouds/cloudy.png
  40. BIN assets/city_scene/clouds/partly_cloudy.png
  41. BIN assets/city_scene/clouds/stormy.png
  42. BIN assets/city_scene/grass/default.png
  43. BIN assets/city_scene/haze/true.png
  44. BIN assets/city_scene/number_sample/1.png
  45. BIN assets/city_scene/number_sample/2.png
  46. BIN assets/city_scene/number_sample/default.png
  47. BIN assets/city_scene/sky/0000.png
  48. BIN assets/city_scene/sky/0200.png
  49. BIN assets/city_scene/sky/0400.png
  50. BIN assets/city_scene/sky/0600.png
  51. BIN assets/city_scene/sky/0800.png
  52. BIN assets/city_scene/sky/1000.png
  53. BIN assets/city_scene/sky/1200.png
  54. BIN assets/city_scene/sky/1400.png
  55. BIN assets/city_scene/sky/1500.png
  56. BIN assets/city_scene/sky/1700.png
  57. BIN assets/city_scene/sky/2000.png
  58. BIN assets/pc306715.jpg
  59. BIN assets/plastik/blue.png
  60. BIN assets/plastik/green.png
  61. BIN assets/plastik/red.png
  62. +26 −0 lib/gruff.rb
  63. +27 −0 lib/gruff/accumulator_bar.rb
  64. +58 −0 lib/gruff/area.rb
  65. +84 −0 lib/gruff/bar.rb
  66. +46 −0 lib/gruff/bar_conversion.rb
  67. +1,070 −0 lib/gruff/base.rb
  68. +109 −0 lib/gruff/bullet.rb
  69. +39 −0 lib/gruff/deprecated.rb
  70. +105 −0 lib/gruff/line.rb
  71. +32 −0 lib/gruff/mini/bar.rb
  72. +77 −0 lib/gruff/mini/legend.rb
  73. +36 −0 lib/gruff/mini/pie.rb
  74. +35 −0 lib/gruff/mini/side_bar.rb
  75. +142 −0 lib/gruff/net.rb
  76. +100 −0 lib/gruff/photo_bar.rb
  77. +124 −0 lib/gruff/pie.rb
  78. +209 −0 lib/gruff/scene.rb
  79. +114 −0 lib/gruff/side_bar.rb
  80. +73 −0 lib/gruff/side_stacked_bar.rb
  81. +130 −0 lib/gruff/spider.rb
  82. +66 −0 lib/gruff/stacked_area.rb
  83. +53 −0 lib/gruff/stacked_bar.rb
  84. +23 −0 lib/gruff/stacked_mixin.rb
  85. +119 −0 test/gruff_test_case.rb
  86. +50 −0 test/test_accumulator_bar.rb
  87. +134 −0 test/test_area.rb
  88. +284 −0 test/test_bar.rb
  89. +8 −0 test/test_base.rb
  90. +26 −0 test/test_bullet.rb
  91. +71 −0 test/test_legend.rb
  92. +493 −0 test/test_line.rb
  93. +32 −0 test/test_mini_bar.rb
  94. +20 −0 test/test_mini_pie.rb
  95. +37 −0 test/test_mini_side_bar.rb
  96. +230 −0 test/test_net.rb
  97. +41 −0 test/test_photo.rb
  98. +154 −0 test/test_pie.rb
  99. +100 −0 test/test_scene.rb
  100. +12 −0 test/test_side_bar.rb
  101. +89 −0 test/test_sidestacked_bar.rb
  102. +216 −0 test/test_spider.rb
  103. +52 −0 test/test_stacked_bar.rb
2 .gitignore
@@ -0,0 +1,2 @@
+.DS_Store
+test/output/*
89 CHANGELOG
@@ -0,0 +1,89 @@
+== 0.3.0
+
+* ???
+
+== 0.2.9
+
+* Patch to make SideBar accurate instead of stacked [Marik]
+* Will be extracting net, pie, stacked, and side-stacked to separate gem
+ in next release.
+
+== 0.2.8
+
+* New accumulator bar graph (experimental)
+* Better mini graphs
+* Bug fixes
+
+== 0.2.7
+
+* Regenerated Manifest.txt
+* Added scene sample to package
+* Added mini side_bar (EXPERIMENTAL)
+* Added @zero_degree option to Gruff::Pie so first slice can start somewhere other than 3 o'clock
+* Increased size of numbers in Gruff::Mini::Pie
+* Added legend_box_size accessor
+
+== 0.2.6
+
+* Fixed missing side_bar.rb in Manifest.txt
+
+== 0.2.5
+
+* New mini graph types (Experimental)
+* Marker lines can be different color than text labels
+* Theme definition cleanup
+
+== 0.2.4
+
+* Added option to hide line numbers
+* Fixed code that was causing warnings
+
+== 0.2.3
+
+* Cleaned up measurements so the graph expands to fill the available space
+* Added x-axis and y-axis label options
+
+== 0.1.2
+
+* minimum_value and maximum_value can be set after data() to manually scale the graph
+* Fixed infinite loop bug when values are all equal
+* Added experimental net and spider graphs
+* Added non-linear scene graph for a simple interface to complex layered graphs
+* Initial refactoring of tests
+* A host of other bug fixes
+
+== 0.0.8
+
+* NEW Sidestacked Bar Graphs. [Alun Eyre]
+* baseline_value larger than data will now show correctly. [Mike Perham]
+* hide_dots and hide_lines are now options for line graphs.
+
+== 0.0.6
+
+* Fixed hang when no data is passed.
+
+== 0.0.4
+
+* Added bar graphs
+* Added area graphs
+* Added pie graphs
+* Added render_image_background for using images as background on a theme
+* Fixed small size legend centering issue
+* Added initial line marker rounding to significant digits (Christian Winkler)
+* Line graphs line width is scaled with number of points being drawn (Christian Winkler)
+
+== 0.0.3
+
+* Added option to draw line graphs without the lines (points only), thanks to Eric Hodel
+* Removed font-minimum check so graphs look better at 300px width
+
+== 0.0.2
+
+* Fixed to_blob (thanks to Carlos Villela)
+* Added bar graphs (initial functionality...will be enhanced)
+* Removed rendered test output from gem
+
+== 0.0.1
+
+* Initial release.
+* Line graphs only. Other graph styles coming soon.
21 MIT-LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2005 Geoffrey Grosenbach boss@topfunky.com
+
+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.
+
72 Manifest.txt
@@ -0,0 +1,72 @@
+CHANGELOG
+MIT-LICENSE
+Manifest.txt
+README.txt
+Rakefile
+assets/bubble.png
+assets/city_scene/background/0000.png
+assets/city_scene/background/0600.png
+assets/city_scene/background/2000.png
+assets/city_scene/clouds/cloudy.png
+assets/city_scene/clouds/partly_cloudy.png
+assets/city_scene/clouds/stormy.png
+assets/city_scene/grass/default.png
+assets/city_scene/haze/true.png
+assets/city_scene/number_sample/1.png
+assets/city_scene/number_sample/2.png
+assets/city_scene/number_sample/default.png
+assets/city_scene/sky/0000.png
+assets/city_scene/sky/0200.png
+assets/city_scene/sky/0400.png
+assets/city_scene/sky/0600.png
+assets/city_scene/sky/0800.png
+assets/city_scene/sky/1000.png
+assets/city_scene/sky/1200.png
+assets/city_scene/sky/1400.png
+assets/city_scene/sky/1500.png
+assets/city_scene/sky/1700.png
+assets/city_scene/sky/2000.png
+assets/pc306715.jpg
+assets/plastik/blue.png
+assets/plastik/green.png
+assets/plastik/red.png
+lib/gruff.rb
+lib/gruff/accumulator_bar.rb
+lib/gruff/area.rb
+lib/gruff/bar.rb
+lib/gruff/bar_conversion.rb
+lib/gruff/base.rb
+lib/gruff/deprecated.rb
+lib/gruff/line.rb
+lib/gruff/mini/bar.rb
+lib/gruff/mini/legend.rb
+lib/gruff/mini/pie.rb
+lib/gruff/mini/side_bar.rb
+lib/gruff/net.rb
+lib/gruff/photo_bar.rb
+lib/gruff/pie.rb
+lib/gruff/scene.rb
+lib/gruff/side_bar.rb
+lib/gruff/side_stacked_bar.rb
+lib/gruff/spider.rb
+lib/gruff/stacked_area.rb
+lib/gruff/stacked_bar.rb
+lib/gruff/stacked_mixin.rb
+test/gruff_test_case.rb
+test/test_accumulator_bar.rb
+test/test_area.rb
+test/test_bar.rb
+test/test_base.rb
+test/test_legend.rb
+test/test_line.rb
+test/test_mini_bar.rb
+test/test_mini_pie.rb
+test/test_mini_side_bar.rb
+test/test_net.rb
+test/test_photo.rb
+test/test_pie.rb
+test/test_scene.rb
+test/test_side_bar.rb
+test/test_sidestacked_bar.rb
+test/test_spider.rb
+test/test_stacked_bar.rb
15 README.txt
@@ -0,0 +1,15 @@
+== Gruff Graphs
+
+A library for making beautiful graphs.
+
+See samples at http://nubyonrails.com/pages/gruff
+
+See the test suite in test/line_test.rb for examples.
+
+== Documentation
+
+Most of the documentation is in the Gruff::Base class.
+
+== WARNING
+
+This is beta-quality software. It works well according to my tests, but the API may change and other features will be added.
52 Rakefile
@@ -0,0 +1,52 @@
+require 'rubygems'
+require 'hoe'
+$:.unshift(File.dirname(__FILE__) + "/lib")
+require 'gruff'
+
+Hoe.new('Gruff', Gruff::VERSION) do |p|
+ p.name = "gruff"
+ p.author = "Geoffrey Grosenbach"
+ p.description = "Beautiful graphs for one or multiple datasets. Can be used on websites or in documents."
+ p.email = 'boss@topfunky.com'
+ p.summary = "Beautiful graphs for one or multiple datasets."
+ p.url = "http://nubyonrails.com/pages/gruff"
+ p.clean_globs = ['test/output/*.png']
+ p.changes = p.paragraphs_of('CHANGELOG', 0..1).join("\n\n")
+ p.remote_rdoc_dir = '' # Release to root
+end
+
+desc "Simple test on packaged files to make sure they are all there"
+task :verify => :package do
+ # An error message will be displayed if files are missing
+ if system %(ruby -e "require 'pkg/gruff-#{Gruff::VERSION}/lib/gruff'")
+ puts "\nThe library files are present"
+ end
+end
+
+namespace :test do
+
+ desc "Run mini tests"
+ task :mini => :clean do
+ Dir['test/test_mini*'].each do |file|
+ system "ruby #{file}"
+ end
+
+ end
+
+end
+
+##
+# Catch unmatched tasks and run them as a unit test.
+#
+# Makes it possible to do
+#
+# rake pie
+#
+# To run the +test/test_pie+ and +test/test_mini_pie+ files.
+
+rule '' do |t|
+ # Rake::Task["clean"].invoke
+ Dir["test/test_*#{t.name}*.rb"].each do |filename|
+ system "ruby #{filename}"
+ end
+end
BIN artwork/city_scene.psd
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
0 artwork/graphs/Backup of Untitled.key/.typeAttributes.dict
No changes.
1 artwork/graphs/Backup of Untitled.key/Contents/PkgInfo
@@ -0,0 +1 @@
+????????
BIN artwork/graphs/Backup of Untitled.key/index.apxl.gz
Binary file not shown.
BIN artwork/graphs/Backup of Untitled.key/thumbs/st0.tiff
Binary file not shown.
BIN artwork/graphs/Backup of Untitled.key/thumbs/st1.tiff
Binary file not shown.
BIN artwork/graphs/Backup of Untitled.key/thumbs/st2.tiff
Binary file not shown.
BIN artwork/graphs/Backup of Untitled.key/thumbs/st3.tiff
Binary file not shown.
0 artwork/graphs/Untitled.key/.typeAttributes.dict
No changes.
1 artwork/graphs/Untitled.key/Contents/PkgInfo
@@ -0,0 +1 @@
+????????
BIN artwork/graphs/Untitled.key/index.apxl.gz
Binary file not shown.
BIN artwork/graphs/Untitled.key/thumbs/st0.tiff
Binary file not shown.
BIN artwork/graphs/Untitled.key/thumbs/st1.tiff
Binary file not shown.
BIN artwork/graphs/Untitled.key/thumbs/st2.tiff
Binary file not shown.
BIN artwork/graphs/Untitled.key/thumbs/st3.tiff
Binary file not shown.
BIN artwork/graphs/samples.001.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN artwork/graphs/samples.002.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN artwork/graphs/samples.003.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN artwork/graphs/samples.004.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 artwork/gruff_controller.rb
@@ -0,0 +1,36 @@
+
+# Handles requests for gruff graphs.
+#
+# You shouldn't need to edit or extend this, but you can read
+# the documentation for GruffHelper to see how to call it from
+# another view.
+#
+# AUTHOR
+# Carlos Villela [mailto:cv@lixo.org]
+# Geoffrey Grosenbach[mailto:boss@topfunky.com]
+#
+# http://lixo.org
+# http://topfunky.com
+#
+require_gem 'gruff'
+
+class GruffController < ApplicationController
+ layout nil
+
+ def index
+ opts = @session['gruff_opts']
+ data = @session['gruff_data']
+
+ raise "No Gruff data or options set in the session" if data.nil? or opts.nil?
+
+ g = Gruff::Line.new(475)
+ g.title = opts[:title]
+ g.labels = opts[:labels]
+ g.theme_37signals
+
+ data.each_pair {|k,v| g.data(k, v) }
+
+ send_data(g.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "gruff.png")
+ end
+
+end
32 artwork/gruffs_helper.rb
@@ -0,0 +1,32 @@
+
+# Provides a tag for embedding gruff graphs into your Rails app.
+#
+# To use, load it in your controller with
+#
+# helper :gruff
+#
+# AUTHOR
+#
+# Carlos Villela [mailto:cv@lixo.org]
+# Geoffrey Grosenbach[mailto:boss@topfunky.com]
+#
+# http://lixo.org
+# http://topfunky.com
+#
+# License
+#
+# This code is licensed under the MIT license.
+#
+module GruffsHelper
+
+ # Call with a name-values hash and an options hash (title and labels supported for now).
+ # You can also pass :class => 'some_css_class' ('gruff' by default).
+ def gruff_tag(data={}, opts={})
+
+ @session['gruff_opts']=opts
+ @session['gruff_data']=data
+
+ "<img src=\"/gruff\" class=\"#{opts[:class] || 'gruff'}\" alt=\"Gruff Graph\" />"
+ end
+
+end
15 artwork/install_rmagick.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+# Install libraries for rmagick, using darwinports.
+#
+# Geoffrey Grosenbach boss@topfunky.com
+#
+
+sudo port install jpeg
+sudo port install libpng
+sudo port install libwmf
+sudo port install tiff
+sudo port install lcms
+sudo port install freetype
+sudo port install imagemagick
+sudo gem install rmagick
BIN artwork/mrplot-0.0.1.tar.gz
Binary file not shown.
BIN artwork/plastik-blue.psd
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN artwork/plastik-green.psd
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN artwork/plastik-red.psd
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN artwork/plastik-template.psd
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN artwork/plastik.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/bubble.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/background/0000.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/background/0600.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/background/2000.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/clouds/cloudy.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/clouds/partly_cloudy.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/clouds/stormy.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/grass/default.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/haze/true.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/number_sample/1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/number_sample/2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/number_sample/default.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/sky/0000.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/sky/0200.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/sky/0400.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN assets/city_scene/sky/0600.png
Diff not rendered.
BIN assets/city_scene/sky/0800.png
Diff not rendered.
BIN assets/city_scene/sky/1000.png
Diff not rendered.
BIN assets/city_scene/sky/1200.png
Diff not rendered.
BIN assets/city_scene/sky/1400.png
Diff not rendered.
BIN assets/city_scene/sky/1500.png
Diff not rendered.
BIN assets/city_scene/sky/1700.png
Diff not rendered.
BIN assets/city_scene/sky/2000.png
Diff not rendered.
BIN assets/pc306715.jpg
Diff not rendered.
BIN assets/plastik/blue.png
Diff not rendered.
BIN assets/plastik/green.png
Diff not rendered.
BIN assets/plastik/red.png
Diff not rendered.
26 lib/gruff.rb
@@ -0,0 +1,26 @@
+# Extra full path added to fix loading errors on some installations.
+
+%w(
+ base
+ area
+ bar
+ bullet
+ line
+ pie
+ spider
+ net
+ stacked_area
+ stacked_bar
+ side_stacked_bar
+ side_bar
+ accumulator_bar
+
+ scene
+
+ mini/legend
+ mini/bar
+ mini/pie
+ mini/side_bar
+).each do |filename|
+ require File.dirname(__FILE__) + "/gruff/#{filename}"
+end
27 lib/gruff/accumulator_bar.rb
@@ -0,0 +1,27 @@
+require File.dirname(__FILE__) + '/base'
+
+##
+# A special bar graph that shows a single dataset as a set of
+# stacked bars. The bottom bar shows the running total and
+# the top bar shows the new value being added to the array.
+
+class Gruff::AccumulatorBar < Gruff::StackedBar
+
+ def draw
+ raise(Gruff::IncorrectNumberOfDatasetsException) unless @data.length == 1
+
+ accumulator_array = []
+ index = 0
+
+ increment_array = @data.first[DATA_VALUES_INDEX].inject([]) {|memo, value|
+ memo << ((index > 0) ? (value + memo.max) : value)
+ accumulator_array << memo[index] - value
+ index += 1
+ memo
+ }
+ data "Accumulator", accumulator_array
+
+ super
+ end
+
+end
58 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
84 lib/gruff/bar.rb
@@ -0,0 +1,84 @@
+
+require File.dirname(__FILE__) + '/base'
+require File.dirname(__FILE__) + '/bar_conversion'
+
+class Gruff::Bar < Gruff::Base
+
+ def draw
+ # Labels will be centered over the left of the bar if
+ # there are more labels than columns. This is basically the same
+ # as where it would be for a line graph.
+ @center_labels_over_point = (@labels.keys.length > @column_count ? true : false)
+
+ super
+ return unless @has_data
+
+ draw_bars
+ end
+
+protected
+
+ def draw_bars
+ # Setup spacing.
+ #
+ # Columns sit side-by-side.
+ spacing_factor = 0.9 # space between the bars
+ @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 explanation
+ 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|
+
+ 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.fill data_row[DATA_COLOR_INDEX]
+ @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)
+ # Subtract half a bar width to center left if requested
+ draw_label(label_center - (@center_labels_over_point ? @bar_width / 2.0 : 0.0), point_index)
+ end
+
+ end
+
+ # Draw the last label if requested
+ draw_label(@graph_right, @column_count) if @center_labels_over_point
+
+ @d.draw(@base_image)
+ end
+
+end
46 lib/gruff/bar_conversion.rb
@@ -0,0 +1,46 @@
+##
+# Original Author: David Stokar
+#
+# This class perfoms the y coordinats conversion for the bar class.
+#
+# There are three cases:
+#
+# 1. Bars all go from zero in positive direction
+# 2. Bars all go from zero to negative direction
+# 3. Bars either go from zero to positive or from zero to negative
+#
+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
1,070 lib/gruff/base.rb
@@ -0,0 +1,1070 @@
+require 'rubygems'
+require 'RMagick'
+require File.dirname(__FILE__) + '/deprecated'
+
+# = 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.
+#
+# See Gruff::Base#theme= for setting themes.
+module Gruff
+
+ # This is the version of Gruff you are using.
+ VERSION = '0.3.0'
+
+ class Base
+
+ include Magick
+ include Deprecated
+
+ # Draw extra lines showing where the margins and text centers are
+ DEBUG = false
+
+ # Used for navigating the array of data to plot
+ DATA_LABEL_INDEX = 0
+ DATA_VALUES_INDEX = 1
+ DATA_COLOR_INDEX = 2
+
+ # Space around text elements. Mostly used for vertical spacing
+ LEGEND_MARGIN = TITLE_MARGIN = LABEL_MARGIN = 10.0
+
+ DEFAULT_TARGET_WIDTH = 800
+
+ # Blank space above the graph
+ attr_accessor :top_margin
+
+ # Blank space below the graph
+ attr_accessor :bottom_margin
+
+ # Blank space to the right of the graph
+ attr_accessor :right_margin
+
+ # Blank space to the left of the graph
+ attr_accessor :left_margin
+
+ # 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
+
+ # Used internally for spacing.
+ #
+ # By default, labels are centered over the point they represent.
+ attr_accessor :center_labels_over_point
+
+ # Used internally for horizontal graph types.
+ attr_accessor :has_left_labels
+
+ # A label for the bottom of the graph
+ attr_accessor :x_axis_label
+
+ # A label for the left side of the graph
+ attr_accessor :y_axis_label
+
+ # attr_accessor :x_axis_increment
+
+ # Manually set increment of the horizontal marking lines
+ attr_accessor :y_axis_increment
+
+ # 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.
+ #
+ # The font= method below fulfills the role of the writer, so we only need
+ # a reader here.
+ attr_reader :font
+
+ attr_accessor :font_color
+
+ # Prevent drawing of line markers
+ attr_accessor :hide_line_markers
+
+ # Prevent drawing of the legend
+ attr_accessor :hide_legend
+
+ # Prevent drawing of the title
+ attr_accessor :hide_title
+
+ # Prevent drawing of line numbers
+ attr_accessor :hide_line_numbers
+
+ # Message shown when there is no data. Fits up to 20 characters. Defaults
+ # to "No Data."
+ attr_accessor :no_data_message
+
+ # The font size of the large title at the top of the graph
+ attr_accessor :title_font_size
+
+ # 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 lines
+ attr_accessor :marker_color
+
+ # The number of horizontal lines shown for reference
+ attr_accessor :marker_count
+
+ # 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
+
+ # Set to false if you don't want the data to be sorted with largest avg
+ # values at the back.
+ attr_accessor :sort
+
+ # Experimental
+ attr_accessor :additional_line_values
+
+ # Experimental
+ attr_accessor :stacked
+
+ # Optionally set the size of the colored box by each item in the legend.
+ # Default is 20.0
+ #
+ # Will be scaled down if graph is smaller than 800px wide.
+ attr_accessor :legend_box_size
+
+ # 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=DEFAULT_TARGET_WIDTH)
+ @top_margin = @bottom_margin = @left_margin = @right_margin = 20.0
+
+ 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
+
+ initialize_ivars
+
+ reset_themes
+ theme_keynote
+ end
+
+ # Set instance variables for this object.
+ #
+ # Subclasses can override this, call super, then set values separately.
+ #
+ # This makes it possible to set defaults in a subclass but still allow
+ # developers to change this values in their program.
+ def initialize_ivars
+ # Internal for calculations
+ @raw_columns = 800.0
+ @raw_rows = 800.0 * (@rows/@columns)
+ @column_count = 0
+ @marker_count = nil
+ @maximum_value = @minimum_value = nil
+ @has_data = false
+ @data = Array.new
+ @labels = Hash.new
+ @labels_seen = Hash.new
+ @sort = true
+ @title = nil
+
+ @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
+
+ @legend_box_size = 20.0
+
+ @no_data_message = "No Data"
+
+ @hide_line_markers = @hide_legend = @hide_title = @hide_line_numbers = false
+ @center_labels_over_point = true
+ @has_left_labels = false
+
+ @additional_line_values = []
+ @additional_line_colors = []
+ @theme_options = {}
+
+ @x_axis_label = @y_axis_label = nil
+ @y_axis_increment = nil
+ @stacked = nil
+ @norm_data = nil
+ end
+
+ # Sets the top, bottom, left and right margins to +margin+.
+ def margins=(margin)
+ @top_margin = @left_margin = @right_margin = @bottom_margin = margin
+ end
+
+ # Sets the font for graph text to the font at +font_path+.
+ def font=(font_path)
+ @font = font_path
+ @d.font = @font
+ 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)
+ reset_themes()
+
+ defaults = {
+ :colors => ['black', 'white'],
+ :additional_line_colors => [],
+ :marker_color => 'white',
+ :font_color => 'black',
+ :background_colors => nil,
+ :background_image => nil
+ }
+ @theme_options = defaults.merge options
+
+ @colors = @theme_options[:colors]
+ @marker_color = @theme_options[:marker_color]
+ @font_color = @theme_options[:font_color] || @marker_color
+ @additional_line_colors = @theme_options[:additional_line_colors]
+
+ render_background
+ end
+
+ # A color scheme similar to the popular presentation software.
+ def theme_keynote
+ # Colors
+ @blue = '#6886B4'
+ @yellow = '#FDD84E'
+ @green = '#72AE6E'
+ @red = '#D1695E'
+ @purple = '#8A6EAF'
+ @orange = '#EFAA43'
+ @white = 'white'
+ @colors = [@yellow, @blue, @green, @red, @purple, @orange, @white]
+
+ self.theme = {
+ :colors => @colors,
+ :marker_color => 'white',
+ :font_color => 'white',
+ :background_colors => ['black', '#4a465a']
+ }
+ end
+
+ # A color scheme plucked from the colors on the popular usability blog.
+ def theme_37signals
+ # Colors
+ @green = '#339933'
+ @purple = '#cc99cc'
+ @blue = '#336699'
+ @yellow = '#FFF804'
+ @red = '#ff0000'
+ @orange = '#cf5910'
+ @black = 'black'
+ @colors = [@yellow, @blue, @green, @red, @purple, @orange, @black]
+
+ self.theme = {
+ :colors => @colors,
+ :marker_color => 'black',
+ :font_color => 'black',
+ :background_colors => ['#d1edf5', 'white']
+ }
+ end
+
+ # A color scheme from the colors used on the 2005 Rails keynote
+ # presentation at RubyConf.
+ def theme_rails_keynote
+ # 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]
+
+ self.theme = {
+ :colors => @colors,
+ :marker_color => 'white',
+ :font_color => 'white',
+ :background_colors => ['#0083a3', '#0083a3']
+ }
+ end
+
+ # A color scheme similar to that used on the popular podcast site.
+ def theme_odeo
+ # 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]
+
+ self.theme = {
+ :colors => @colors,
+ :marker_color => 'white',
+ :font_color => 'white',
+ :background_colors => ['#ff47a4', '#ff1f81']
+ }
+ end
+
+ # A pastel theme
+ def theme_pastel
+ # Colors
+ @colors = [
+ '#a9dada', # blue
+ '#aedaa9', # green
+ '#daaea9', # peach
+ '#dadaa9', # yellow
+ '#a9a9da', # dk purple
+ '#daaeda', # purple
+ '#dadada' # grey
+ ]
+
+ self.theme = {
+ :colors => @colors,
+ :marker_color => '#aea9a9', # Grey
+ :font_color => 'black',
+ :background_colors => 'white'
+ }
+ end
+
+ # A greyscale theme
+ def theme_greyscale
+ # Colors
+ @colors = [
+ '#282828', #
+ '#383838', #
+ '#686868', #
+ '#989898', #
+ '#c8c8c8', #
+ '#e8e8e8', #
+ ]
+
+ self.theme = {
+ :colors => @colors,
+ :marker_color => '#aea9a9', # Grey
+ :font_color => 'black',
+ :background_colors => 'white'
+ }
+ 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_points = Array(data_points) # make sure it's an array
+ @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
+ @has_data = true if @maximum_value > 0
+
+ @minimum_value = less_than_min?(data_point) ? data_point : @minimum_value
+ @has_data = true if @minimum_value < 0
+ 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
+
+protected
+
+ # 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
+
+ debug {
+ # Outer margin
+ @d.rectangle( @left_margin, @top_margin,
+ @raw_columns - @right_margin, @raw_rows - @bottom_margin)
+ # Graph area box
+ @d.rectangle( @graph_left, @graph_top, @graph_right, @graph_bottom)
+ }
+ end
+
+ # Calculates size of drawable area and 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() if @sort # Sort norm_data with avg largest values set first (for display)
+
+ draw_legend()
+ draw_line_markers()
+ draw_axis_labels()
+ draw_title
+ end
+
+ # Make copy of data with values scaled between 0-100
+ def normalize(force=false)
+ if @norm_data.nil? || force
+ @norm_data = []
+ return unless @has_data
+
+ calculate_spread
+
+ @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 - @minimum_value.to_f ) / @spread)
+ end
+ end
+ @norm_data << [data_row[DATA_LABEL_INDEX], norm_data_points, data_row[DATA_COLOR_INDEX]]
+ end
+ end
+ end
+
+ def calculate_spread # :nodoc:
+ @spread = @maximum_value.to_f - @minimum_value.to_f
+ @spread = @spread > 0 ? @spread : 1
+ end
+
+ # Calculates size of drawable area, general font dimensions, etc.
+ def setup_graph_measurements
+ @marker_caps_height = @hide_line_markers ? 0 :
+ calculate_caps_height(@marker_font_size)
+ @title_caps_height = @hide_title ? 0 :
+ calculate_caps_height(@title_font_size)
+ @legend_caps_height = @hide_legend ? 0 :
+ calculate_caps_height(@legend_font_size)
+
+ if @hide_line_markers
+ (@graph_left,
+ @graph_right_margin,
+ @graph_bottom_margin) = [@left_margin, @right_margin, @bottom_margin]
+ else
+ longest_left_label_width = 0
+ if @has_left_labels
+ longest_left_label_width = calculate_width(@marker_font_size,
+ labels.values.inject('') { |value, memo| (value.to_s.length > memo.to_s.length) ? value : memo }) * 1.25
+ else
+ longest_left_label_width = calculate_width(@marker_font_size,
+ label(@maximum_value.to_f))
+ end
+
+ # Shift graph if left line numbers are hidden
+ line_number_width = @hide_line_numbers && !@has_left_labels ?
+ 0.0 :
+ (longest_left_label_width + LABEL_MARGIN * 2)
+
+ @graph_left = @left_margin +
+ line_number_width +
+ (@y_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN * 2)
+ # Make space for half the width of the rightmost column label.
+ # Might be greater than the number of columns if between-style bar markers are used.
+ last_label = @labels.keys.sort.last.to_i
+ extra_room_for_long_label = (last_label >= (@column_count-1) && @center_labels_over_point) ?
+ calculate_width(@marker_font_size, @labels[last_label])/2.0 :
+ 0
+ @graph_right_margin = @right_margin + extra_room_for_long_label
+
+ @graph_bottom_margin = @bottom_margin +
+ @marker_caps_height + LABEL_MARGIN
+ end
+
+ @graph_right = @raw_columns - @graph_right_margin
+ @graph_width = @raw_columns - @graph_left - @graph_right_margin
+
+ # When @hide title, leave a TITLE_MARGIN space for aesthetics.
+ # Same with @hide_legend
+ @graph_top = @top_margin +
+ (@hide_title ? TITLE_MARGIN : @title_caps_height + TITLE_MARGIN * 2) +
+ (@hide_legend ? LEGEND_MARGIN : @legend_caps_height + LEGEND_MARGIN * 2)
+
+ x_axis_label_height = @x_axis_label.nil? ? 0.0 :
+ @marker_caps_height + LABEL_MARGIN
+ @graph_bottom = @raw_rows - @graph_bottom_margin - x_axis_label_height
+ @graph_height = @graph_bottom - @graph_top
+ end
+
+ # Draw the optional labels for the x axis and y axis.
+ def draw_axis_labels
+ unless @x_axis_label.nil?
+ # X Axis
+ # Centered vertically and horizontally by setting the
+ # height to 1.0 and the width to the width of the graph.
+ x_axis_label_y_coordinate = @graph_bottom + LABEL_MARGIN * 2 + @marker_caps_height
+
+ # TODO Center between graph area
+ @d.fill = @font_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,
+ @raw_columns, 1.0,
+ 0.0, x_axis_label_y_coordinate,
+ @x_axis_label, @scale)
+ debug { @d.line 0.0, x_axis_label_y_coordinate, @raw_columns, x_axis_label_y_coordinate }
+ end
+
+ unless @y_axis_label.nil?
+ # Y Axis, rotated vertically
+ @d.rotation = 90.0
+ @d.gravity = CenterGravity
+ @d = @d.annotate_scaled( @base_image,
+ 1.0, @raw_rows,
+ @left_margin + @marker_caps_height / 2.0, 0.0,
+ @y_axis_label, @scale)
+ @d.rotation = -90.0
+ end
+ end
+
+ # Draws horizontal background lines and labels
+ def draw_line_markers
+ return if @hide_line_markers
+
+ @d = @d.stroke_antialias false
+
+ if @y_axis_increment.nil?
+ # Try to use a number of horizontal lines that will come out even.
+ #
+ # TODO Do the same for larger numbers...100, 75, 50, 25
+ if @marker_count.nil?
+ (3..7).each do |lines|
+ if @spread % lines == 0.0
+ @marker_count = lines
+ break
+ end
+ end
+ @marker_count ||= 4
+ end
+ @increment = (@spread > 0) ? significant(@spread / @marker_count) : 1
+ else
+ # TODO Make this work for negative values
+ @maximum_value = [@maximum_value.ceil, @y_axis_increment].max
+ @minimum_value = @minimum_value.floor
+ calculate_spread
+ normalize(true)
+
+ @marker_count = (@spread / @y_axis_increment).to_i
+ @increment = @y_axis_increment
+ end
+ @increment_scaled = @graph_height.to_f / (@spread / @increment)
+
+ # Draw horizontal line markers and annotate with numbers
+ (0..@marker_count).each do |index|
+ y = @graph_top + @graph_height - index.to_f * @increment_scaled
+
+ @d = @d.stroke(@marker_color)
+ @d = @d.stroke_width 1
+ @d = @d.line(@graph_left, y, @graph_right, y)
+
+ marker_label = index * @increment + @minimum_value.to_f
+
+ unless @hide_line_numbers
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.stroke('transparent')
+ @d.pointsize = scale_fontsize(@marker_font_size)
+ @d.gravity = EastGravity
+
+ # Vertically center with 1.0 for the height
+ @d = @d.annotate_scaled( @base_image,
+ @graph_left - LABEL_MARGIN, 1.0,
+ 0.0, y,
+ label(marker_label), @scale)
+ end
+ end
+
+ # # Submitted by a contibutor...the utility escapes me
+ # i = 0
+ # @additional_line_values.each do |value|
+ # @increment_scaled = @graph_height.to_f / (@maximum_value.to_f / value)
+ #
+ # y = @graph_top + @graph_height - @increment_scaled
+ #
+ # @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
+
+ @d = @d.stroke_antialias true
+ 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 = @legend_box_size # 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
+ current_y_offset = @hide_title ?
+ @top_margin + LEGEND_MARGIN :
+ @top_margin +
+ TITLE_MARGIN + @title_caps_height +
+ LEGEND_MARGIN
+
+ debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
+
+ @legend_labels.each_with_index do |legend_label, index|
+
+ # Draw label
+ @d.fill = @font_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, 1.0,
+ current_x_offset + (legend_square_width * 1.7), current_y_offset,
+ legend_label.to_s, @scale)
+
+ # Now draw box with color of this dataset
+ @d = @d.stroke('transparent')
+ @d = @d.fill @data[index][DATA_COLOR_INDEX]
+ @d = @d.rectangle(current_x_offset,
+ current_y_offset - legend_square_width / 2.0,
+ current_x_offset + legend_square_width,
+ current_y_offset + legend_square_width / 2.0)
+
+ @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
+
+ # Draws a title on the graph.
+ def draw_title
+ return if (@hide_title || @title.nil?)
+
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.stroke('transparent')
+ @d.pointsize = scale_fontsize(@title_font_size)
+ @d.font_weight = BoldWeight
+ @d.gravity = NorthGravity
+ @d = @d.annotate_scaled( @base_image,
+ @raw_columns, 1.0,
+ 0, @top_margin,
+ @title, @scale)
+ end
+
+ # Draws column labels below graph, centered over x_offset
+ #--
+ # TODO Allow WestGravity as an option
+ def draw_label(x_offset, index)
+ return if @hide_line_markers
+
+ if !@labels[index].nil? && @labels_seen[index].nil?
+ y_offset = @graph_bottom + LABEL_MARGIN
+
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.stroke('transparent')
+ @d.font_weight = NormalWeight
+ @d.pointsize = scale_fontsize(@marker_font_size)
+ @d.gravity = NorthGravity
+ @d = @d.annotate_scaled(@base_image,
+ 1.0, 1.0,
+ x_offset, y_offset,
+ @labels[index], @scale)
+ @labels_seen[index] = 1
+ debug { @d.line 0.0, y_offset, @raw_columns, y_offset }
+ end
+ end
+
+ # Shows an error message because you have no data.
+ def draw_no_data
+ @d.fill = @font_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
+
+ # Finds the best background to render based on the provided theme options.
+ #
+ # Creates a @base_image to draw on.
+ def render_background
+ case @theme_options[:background_colors]
+ when Array
+ @base_image = render_gradiated_background(*@theme_options[:background_colors])
+ when String
+ @base_image = render_solid_background(@theme_options[:background_colors])
+ else
+ @base_image = render_image_background(*@theme_options[:background_image])
+ end
+ end
+
+ # Make a new image at the current size with a solid +color+.
+ def render_solid_background(color)
+ Image.new(@columns, @rows) {
+ self.background_color = color
+ }
+ end
+
+ # Use with a theme definition method to draw a gradiated 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
+
+ # Resets everything to defaults (except data).
+ def reset_themes
+ @color_index = 0
+ @labels_seen = {}
+ @theme_options = {}
+
+ @d = Draw.new
+ # Scale down from 800x600 used to calculate drawing.
+ @d = @d.scale(@scale, @scale)
+ end
+
+ def scale(value) # :nodoc:
+ value * @scale
+ end
+
+ # Return a comparable fontsize for the current graph.
+ def scale_fontsize(value)
+ new_fontsize = value * @scale
+ # return new_fontsize < 10.0 ? 10.0 : new_fontsize
+ return new_fontsize
+ end
+
+ def clip_value_if_greater_than(value, max_value) # :nodoc:
+ (value > max_value) ? max_value : value
+ end
+
+ # Overridden by subclasses such as stacked bar.
+ def larger_than_max?(data_point, index=0) # :nodoc:
+ data_point > @maximum_value
+ end
+
+ def less_than_min?(data_point, index=0) # :nodoc:
+ data_point < @minimum_value
+ end
+
+ # Overridden by subclasses that need it.
+ def max(data_point, index) # :nodoc:
+ data_point
+ end
+
+ # Overridden by subclasses that need it.
+ def min(data_point, index) # :nodoc:
+ data_point
+ end
+
+ def significant(inc) # :nodoc:
+ 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) # :nodoc:
+ total_sum = 0
+ data_set.collect {|num| total_sum += num.to_f }
+ total_sum
+ end
+
+ # Used by StackedBar and child classes.
+ #
+ # May need to be moved to the StackedBar class.
+ 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
+
+ def make_stacked # :nodoc:
+ 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
+
+private
+
+ # Takes a block and draws it if DEBUG is true.
+ #
+ # Example:
+ # debug { @d.rectangle x1, y1, x2, y2 }
+ def debug
+ if DEBUG
+ @d = @d.fill 'transparent'
+ @d = @d.stroke 'turquoise'
+ @d = yield
+ end
+ end
+
+ # Uses the next color in your color list.
+ 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]
+ end
+ end
+ end
+
+ # Return a formatted string representing a number value that should be
+ # printed as a label.
+ def label(value)
+ if (@spread.to_f % @marker_count.to_f == 0) || !@y_axis_increment.nil?
+ return value.to_i.to_s
+ end
+
+ if @spread > 10.0
+ sprintf("%0i", value)
+ elsif @spread >= 3.0
+ sprintf("%0.2f", value)
+ else
+ value.to_s
+ end
+ end
+
+ # Returns the height of the capital letter 'X' for the current font and
+ # size.
+ #
+ # Not scaled since it deals with dimensions that the regular scaling will
+ # handle.
+ def calculate_caps_height(font_size)
+ @d.pointsize = font_size
+ @d.get_type_metrics(@base_image, 'X').height
+ end
+
+ # Returns the width of a string at this pointsize.
+ #
+ # Not scaled since it deals with dimensions that the regular
+ # scaling will handle.
+ def calculate_width(font_size, text)
+ @d.pointsize = font_size
+ @d.get_type_metrics(@base_image, text.to_s).width
+ end
+
+ end # Gruff::Base
+
+ class IncorrectNumberOfDatasetsException < StandardError; end
+
+end # Gruff
+
+module Magick
+
+ class Draw
+
+ # Additional method to scale annotation text since Draw.scale doesn't.
+ 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 # Magick
+
109 lib/gruff/bullet.rb
@@ -0,0 +1,109 @@
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::Bullet < Gruff::Base
+
+ def initialize(target_width="400x40")
+ 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 / 5.0
+ end
+
+ initialize_ivars
+
+ reset_themes
+ theme_greyscale
+ @title_font_size = 20
+ end
+
+ def data(value, maximum_value, options={})
+ @value = value.to_f
+ @maximum_value = maximum_value.to_f
+ @options = options
+ @options.map { |k, v| @options[k] = v.to_f if v === Numeric }
+ end
+
+ # 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() if @sort # Sort norm_data with avg largest values set first (for display)
+ #
+ # draw_legend()
+ # draw_line_markers()
+ # draw_axis_labels()
+ # draw_title
+ # end
+
+ def draw
+ # TODO Left label
+ # TODO Bottom labels and markers
+ # @graph_bottom
+ # Calculations are off 800x???
+
+ @colors.reverse!
+
+ draw_title
+
+ @margin = 30.0
+ @thickness = @raw_rows / 6.0
+ @right_margin = @margin
+ @graph_left = @title_width * 1.3 rescue @margin # HACK Need to calculate real width
+ @graph_width = @raw_columns - @graph_left - @right_margin
+ @graph_height = @thickness * 3.0
+
+ # Background
+ @d = @d.fill @colors[0]
+ @d = @d.rectangle(@graph_left, 0, @graph_left + @graph_width, @graph_height)
+
+ [:high, :low].each_with_index do |indicator, index|
+ next unless @options.has_key?(indicator)
+ @d = @d.fill @colors[index + 1]
+ indicator_width_x = @graph_left + @graph_width * (@options[indicator] / @maximum_value)
+ @d = @d.rectangle(@graph_left, 0, indicator_width_x, @graph_height)
+ end
+
+ if @options.has_key?(:target)
+ @d = @d.fill @font_color
+ target_x = @graph_left + @graph_width * (@options[:target] / @maximum_value)
+ half_thickness = @thickness / 2.0
+ @d = @d.rectangle(target_x, half_thickness, target_x + half_thickness, @thickness * 2 + half_thickness)
+ end
+
+ # Value
+ @d = @d.fill @font_color
+ @d = @d.rectangle(@graph_left, @thickness, @graph_left + @graph_width * (@value / @maximum_value), @thickness * 2)
+
+ @d.draw(@base_image)
+ end
+
+ def draw_title
+ return unless @title
+
+ @font_height = calculate_caps_height(scale_fontsize(@title_font_size))
+ @title_width = calculate_width(@title_font_size, @title)
+
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.stroke('transparent')
+ @d.font_weight = NormalWeight
+ @d.pointsize = scale_fontsize(@title_font_size)
+ @d.gravity = NorthWestGravity
+ @d = @d.annotate_scaled(*[
+ @base_image,
+ 1.0, 1.0,
+ @font_height/2, @font_height/2,
+ @title,
+ @scale
+ ])
+ end
+
+end
39 lib/gruff/deprecated.rb
@@ -0,0 +1,39 @@
+
+##
+# A mixin for methods that need to be deleted or have been
+# replaced by cleaner code.
+
+module Gruff
+ module Deprecated
+
+ 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
+
+ # TODO Should be calculate_graph_height
+ # def setup_graph_height
+ # @graph_height = @graph_bottom - @graph_top
+ # end
+
+ end
+end
105 lib/gruff/line.rb
@@ -0,0 +1,105 @@
+
+require File.dirname(__FILE__) + '/base'
+
+##
+# Here's how to make a Line graph:
+#
+# g = Gruff::Line.new
+# g.title = "A Line Graph"
+# g.data 'Fries', [20, 23, 19, 8]
+# g.data 'Hamburgers', [50, 19, 99, 29]
+# g.write("test/output/line.png")
+#
+# There are also other options described below, such as #baseline_value, #baseline_color, #hide_dots, and #hide_lines.
+
+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'
+ @baseline_value = nil
+ end
+
+ def draw
+ super
+
+ return unless @has_data
+
+ # Check to see if more than one datapoint was given. NaN can result otherwise.
+ @x_increment = (@column_count > 1) ? (@graph_width / (@column_count - 1).to_f) : @graph_width
+
+ 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
+
+ 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)
+
+ # Reset each time to avoid thin-line errors
+ @d = @d.stroke data_row[DATA_COLOR_INDEX]
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
+ @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 !@hide_lines and !prev_x.nil? and !prev_y.nil? then
+ @d = @d.line(prev_x, prev_y, new_x, new_y)
+ end
+ circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
+ @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 = [@maximum_value.to_f, @baseline_value.to_f].max
+ super
+ @norm_baseline = (@baseline_value.to_f / @maximum_value.to_f) if @baseline_value
+ end
+
+end
32 lib/gruff/mini/bar.rb
@@ -0,0 +1,32 @@
+##
+#
+# Makes a small bar graph suitable for display at 200px or even smaller.
+#
+module Gruff
+ module Mini
+
+ class Bar < Gruff::Bar
+
+ include Gruff::Mini::Legend
+
+ def draw
+ @hide_legend = true
+ @hide_title = true
+ @hide_line_numbers = true
+
+ @marker_font_size = 50.0
+ @minimum_value = 0.0
+ @legend_font_size = 60.0
+
+ expand_canvas_for_vertical_legend
+
+ super
+
+ draw_vertical_legend
+ @d.draw(@base_image)
+ end
+
+ end
+
+ end
+end
77 lib/gruff/mini/legend.rb
@@ -0,0 +1,77 @@
+module Gruff
+ module Mini
+ module Legend
+
+ ##
+ # The canvas needs to be bigger so we can put the legend beneath it.
+
+ def expand_canvas_for_vertical_legend
+ @original_rows = @raw_rows
+ @rows += @data.length * calculate_caps_height(scale_fontsize(@legend_font_size)) * 1.7
+ render_background
+ end
+
+ ##
+ # Draw the legend beneath the existing graph.
+
+ def draw_vertical_legend
+
+ @legend_labels = @data.collect {|item| item[Gruff::Base::DATA_LABEL_INDEX] }
+
+ legend_square_width = 40.0 # small square with color of this item
+ legend_square_margin = 10.0
+ @legend_left_margin = 100.0
+ legend_top_margin = 40.0
+
+ # May fix legend drawing problem at small sizes
+ @d.font = @font if @font
+ @d.pointsize = @legend_font_size
+
+ current_x_offset = @legend_left_margin
+ current_y_offset = @original_rows + legend_top_margin
+
+ debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
+
+ @legend_labels.each_with_index do |legend_label, index|
+
+ # Draw label
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.pointsize = scale_fontsize(@legend_font_size)
+ @d.stroke = 'transparent'
+ @d.font_weight = Magick::NormalWeight
+ @d.gravity = Magick::WestGravity
+ @d = @d.annotate_scaled( @base_image,
+ @raw_columns, 1.0,
+ current_x_offset + (legend_square_width * 1.7), current_y_offset,
+ truncate_legend_label(legend_label), @scale)
+
+ # Now draw box with color of this dataset
+ @d = @d.stroke 'transparent'
+ @d = @d.fill @data[index][Gruff::Base::DATA_COLOR_INDEX]
+ @d = @d.rectangle(current_x_offset,
+ current_y_offset - legend_square_width / 2.0,
+ current_x_offset + legend_square_width,
+ current_y_offset + legend_square_width / 2.0)
+
+ current_y_offset += calculate_caps_height(@legend_font_size) * 1.7
+ end
+ @color_index = 0