Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

first commit

  • Loading branch information...
commit 92783f84aab66b76a09405cffbe5ac5e869a1839 0 parents
Delano Mandelbaum delano authored
Showing with 12,616 additions and 0 deletions.
  1. +104 −0 CHANGES
  2. +109 −0 History.txt
  3. +20 −0 License.txt
  4. +20 −0 MIT-LICENSE
  5. +104 −0 Manifest.txt
  6. +6 −0 PostInstall.txt
  7. +9 −0 README
  8. +66 −0 README.txt
  9. +108 −0 Rakefile
  10. +78 −0 config/hoe.rb
  11. +15 −0 config/requirements.rb
  12. +30 −0 lib/scruffy.rb
  13. +21 −0 lib/scruffy/components.rb
  14. +24 −0 lib/scruffy/components/background.rb
  15. +54 −0 lib/scruffy/components/base.rb
  16. +26 −0 lib/scruffy/components/data_markers.rb
  17. +48 −0 lib/scruffy/components/graphs.rb
  18. +16 −0 lib/scruffy/components/grid.rb
  19. +17 −0 lib/scruffy/components/label.rb
  20. +105 −0 lib/scruffy/components/legend.rb
  21. +22 −0 lib/scruffy/components/style_info.rb
  22. +19 −0 lib/scruffy/components/title.rb
  23. +32 −0 lib/scruffy/components/value_markers.rb
  24. +37 −0 lib/scruffy/components/viewport.rb
  25. +195 −0 lib/scruffy/formatters.rb
  26. +189 −0 lib/scruffy/graph.rb
  27. +24 −0 lib/scruffy/graph_state.rb
  28. +12 −0 lib/scruffy/helpers.rb
  29. +41 −0 lib/scruffy/helpers/canvas.rb
  30. +95 −0 lib/scruffy/helpers/layer_container.rb
  31. +5 −0 lib/scruffy/helpers/meta.rb
  32. +70 −0 lib/scruffy/helpers/point_container.rb
  33. +24 −0 lib/scruffy/layers.rb
  34. +137 −0 lib/scruffy/layers/all_smiles.rb
  35. +46 −0 lib/scruffy/layers/area.rb
  36. +67 −0 lib/scruffy/layers/average.rb
  37. +52 −0 lib/scruffy/layers/bar.rb
  38. +180 −0 lib/scruffy/layers/base.rb
  39. +29 −0 lib/scruffy/layers/line.rb
  40. +123 −0 lib/scruffy/layers/pie.rb
  41. +119 −0 lib/scruffy/layers/pie_slice.rb
  42. +21 −0 lib/scruffy/layers/scatter.rb
  43. +39 −0 lib/scruffy/layers/sparkline_bar.rb
  44. +87 −0 lib/scruffy/layers/stacked.rb
  45. +14 −0 lib/scruffy/rasterizers.rb
  46. +39 −0 lib/scruffy/rasterizers/batik_rasterizer.rb
  47. +27 −0 lib/scruffy/rasterizers/rmagick_rasterizer.rb
  48. +22 −0 lib/scruffy/renderers.rb
  49. +93 −0 lib/scruffy/renderers/base.rb
  50. +44 −0 lib/scruffy/renderers/cubed.rb
  51. +53 −0 lib/scruffy/renderers/cubed3d.rb
  52. +22 −0 lib/scruffy/renderers/empty.rb
  53. +20 −0 lib/scruffy/renderers/pie.rb
  54. +17 −0 lib/scruffy/renderers/reversed.rb
  55. +10 −0 lib/scruffy/renderers/sparkline.rb
  56. +48 −0 lib/scruffy/renderers/split.rb
  57. +36 −0 lib/scruffy/renderers/standard.rb
  58. +141 −0 lib/scruffy/themes.rb
  59. +9 −0 lib/scruffy/version.rb
  60. +10 −0 script/console
  61. +14 −0 script/destroy
  62. +14 −0 script/generate
  63. +82 −0 script/txt2html
  64. +1,585 −0 setup.rb
  65. +175 −0 spec/scruffy/graph_spec.rb
  66. +30 −0 spec/scruffy/layers/base_spec.rb
  67. +10 −0 spec/scruffy/layers/line_spec.rb
  68. +8 −0 spec/spec_helper.rb
  69. +34 −0 tasks/deployment.rake
  70. +7 −0 tasks/environment.rake
  71. +17 −0 tasks/website.rake
  72. +101 −0 test/graph_creation_test.rb
  73. +2 −0  test/test_helper.rb
  74. +7 −0 website/images/blank.gif.html
  75. BIN  website/images/graphs/all_smiles.png
  76. BIN  website/images/graphs/bar_test.png
  77. +71 −0 website/images/graphs/bar_test.svg
  78. BIN  website/images/graphs/line_test.png
  79. +60 −0 website/images/graphs/line_test.svg
  80. BIN  website/images/graphs/multi_test.png
  81. +296 −0 website/images/graphs/multi_test.svg
  82. BIN  website/images/graphs/pie_test.png
  83. +40 −0 website/images/graphs/pie_test.svg
  84. BIN  website/images/graphs/split_test.png
  85. +295 −0 website/images/graphs/split_test.svg
  86. BIN  website/images/graphs/stacking_test.png
  87. +146 −0 website/images/graphs/stacking_test.svg
  88. BIN  website/images/header.png
  89. BIN  website/images/header_gradient.png
  90. BIN  website/images/overlay.png
  91. BIN  website/images/scruffy.png
  92. +304 −0 website/index.html
  93. +204 −0 website/index.txt
  94. +2 −0  website/javascripts/application.js
  95. +815 −0 website/javascripts/controls.js
  96. +913 −0 website/javascripts/dragdrop.js
  97. +958 −0 website/javascripts/effects.js
  98. +437 −0 website/javascripts/lightbox.js
  99. +2,006 −0 website/javascripts/prototype.js
  100. +285 −0 website/javascripts/rounded_corners_lite.inc.js
  101. +27 −0 website/stylesheets/lightbox.css
  102. +147 −0 website/stylesheets/screen.css
  103. +227 −0 website/stylesheets/scruffy.css
  104. +47 −0 website/template.html.erb
104 CHANGES
@@ -0,0 +1,104 @@
+= Scruffy Changelog
+== Version 0.2.3
+(July 4th, 2008)
+* Got pie charts working
+* Added rough capability for legend to run vertically (better for Pie chart)
+* Added some checks in the Pie initializer so that you simply pass a Hash of
+ name => values, instead of adding PieSlices in a block.
+* Added a simplistic unit test that outputs a pie and line chart in PNG & SVG.
+* Quite a lot of hard-wired values in here. Whole thing needs a spring clean.
+
+== Version 0.2.2
+(August 19th, 2006)
+
+* Removed all font-family and text-rendering attributes from elements.
+ - These were causing issues with Batik and Adobe Viewer. Horrible font problems.
+* Added require 'builder' to renderers/base.rb
+* Added minor shadows to most graph types. Adds some depth perception.
+* Added graph.layout as an alias of renderer. (graph.layout looks nicer).
+* Added markers, values, grid options.
+
+== Version 0.2.1
+(August 18th, 2006)
+
+* Mostly documentation.
+* Added Builder 2.0 dependency to gem spec.
+* Removed minimum size hack in RMagickRasterizer, for now.
+
+
+== Version 0.2.0
+(August 14th, 2006)
+
+- Lots of changes, hold on tight:
+
+* Redesigned rendering system to a component-based design.
+ All objects on the canvas are components that can be re-arranged via renderers.
+* Created default renderer for basic Gruff-like layout.
+* Added Reversed and Cubed renderers to demonstrate the customization abilities (plus, they're cool).
+* Added Split renderer.
+* Created Viewport component to help with Cubed.
+ - Viewport lets you scale it's inner components and move around the
+ graph. Its components' sizes and positions are relative to the viewport,
+ not the graph.
+* Set title to respect marker color if available.
+* Respects :to option in Graph#render for SVG output to file.
+* Stacked layer type -- accepts layers which it then uses to create a stacked graph. Such as Bar graphs
+ and Area graphs.
+* Abstracted out layer_container functionality to helper module (for stacked graph)
+* Renamed value_transformers to value_formatters.
+* Refined Value Formatters.
+ - Created default: Number.
+ - Respects float precision
+ - Allows for "auto-precision", which will use the largest precision (up to a customizable limit)
+ necessary to portray the values correctly. ie: 5.1, 6.32, 7.142 becomes '5.100', '6.320', '7.142'
+* Modified Legend component, Layers, and Graph component to respect categories.
+ - ie: Creating a Bar layer with :category => :sales and a Graph with :category => :qa will result in
+ the Bay layer not being displayed. Allows for more than one Graph viewport on a screen with different
+ layers.
+* Improved rasterizing at smaller sizes( < 300px) by rasterizing the image at a larger size first, then
+ allowing RMagick to resize the image with specific filtering/blurring. Actually looks better than just
+ rasterizing the SVG at the small size from the beginning.
+* Fixed Opacity on stacked graphs.
+* Added Style (invisible) components to allow for CSS styling. (Not recommended, however.)
+* Added Label component for arbitrary text.
+* Created Theme object in place of theme hash.
+
+== Version 0.1.0
+(August 11th, 2006)
+
+* First public release!
+* Legend rendering
+* Rasterizing graph to multiple image types (graph.render :as => 'PNG')
+
+== Version 0.0.12
+(August 10th, 2006)
+This is not a public release.
+
+* Rearranged Layers into a better class/module arrangement.
+
+== Version 0.0.11
+(August 10th, 2006)
+This is not a public release.
+
+* Fixed gem issue.
+
+== Version 0.0.10
+(August 10th, 2006)
+
+This is not a public release.
+
+
+* Removed bogus changelog.
+
+
+== Version 0.0.9
+(August 10th, 2006)
+
+This is not a public release.
+
+* Initial release.
+* Standard renderer.
+* Marker transformers: currency, percentages.
+* Basic Graphs: Area, Bar, Line.
+* Advanced Graphs: Average, AllSmiles.
+* Initial documentation.
109 History.txt
@@ -0,0 +1,109 @@
+= Scruffy Changelog
+== Version 0.2.4
+(2008-08-22)
+* Bug #21517, legend text missing, fixed.
+* Bug #21604, Fails to properly require RMagick, fixed.
+
+== Version 0.2.3
+(July 4th, 2008)
+* Got pie charts working
+* Added rough capability for legend to run vertically (better for Pie chart)
+* Added some checks in the Pie initializer so that you simply pass a Hash of
+ name => values, instead of adding PieSlices in a block.
+* Added a simplistic unit test that outputs a pie and line chart in PNG & SVG.
+* Quite a lot of hard-wired values in here. Whole thing needs a spring clean.
+
+== Version 0.2.2
+(August 19th, 2006)
+
+* Removed all font-family and text-rendering attributes from elements.
+ - These were causing issues with Batik and Adobe Viewer. Horrible font problems.
+* Added require 'builder' to renderers/base.rb
+* Added minor shadows to most graph types. Adds some depth perception.
+* Added graph.layout as an alias of renderer. (graph.layout looks nicer).
+* Added markers, values, grid options.
+
+== Version 0.2.1
+(August 18th, 2006)
+
+* Mostly documentation.
+* Added Builder 2.0 dependency to gem spec.
+* Removed minimum size hack in RMagickRasterizer, for now.
+
+
+== Version 0.2.0
+(August 14th, 2006)
+
+- Lots of changes, hold on tight:
+
+* Redesigned rendering system to a component-based design.
+ All objects on the canvas are components that can be re-arranged via renderers.
+* Created default renderer for basic Gruff-like layout.
+* Added Reversed and Cubed renderers to demonstrate the customization abilities (plus, they're cool).
+* Added Split renderer.
+* Created Viewport component to help with Cubed.
+ - Viewport lets you scale it's inner components and move around the
+ graph. Its components' sizes and positions are relative to the viewport,
+ not the graph.
+* Set title to respect marker color if available.
+* Respects :to option in Graph#render for SVG output to file.
+* Stacked layer type -- accepts layers which it then uses to create a stacked graph. Such as Bar graphs
+ and Area graphs.
+* Abstracted out layer_container functionality to helper module (for stacked graph)
+* Renamed value_transformers to value_formatters.
+* Refined Value Formatters.
+ - Created default: Number.
+ - Respects float precision
+ - Allows for "auto-precision", which will use the largest precision (up to a customizable limit)
+ necessary to portray the values correctly. ie: 5.1, 6.32, 7.142 becomes '5.100', '6.320', '7.142'
+* Modified Legend component, Layers, and Graph component to respect categories.
+ - ie: Creating a Bar layer with :category => :sales and a Graph with :category => :qa will result in
+ the Bay layer not being displayed. Allows for more than one Graph viewport on a screen with different
+ layers.
+* Improved rasterizing at smaller sizes( < 300px) by rasterizing the image at a larger size first, then
+ allowing RMagick to resize the image with specific filtering/blurring. Actually looks better than just
+ rasterizing the SVG at the small size from the beginning.
+* Fixed Opacity on stacked graphs.
+* Added Style (invisible) components to allow for CSS styling. (Not recommended, however.)
+* Added Label component for arbitrary text.
+* Created Theme object in place of theme hash.
+
+== Version 0.1.0
+(August 11th, 2006)
+
+* First public release!
+* Legend rendering
+* Rasterizing graph to multiple image types (graph.render :as => 'PNG')
+
+== Version 0.0.12
+(August 10th, 2006)
+This is not a public release.
+
+* Rearranged Layers into a better class/module arrangement.
+
+== Version 0.0.11
+(August 10th, 2006)
+This is not a public release.
+
+* Fixed gem issue.
+
+== Version 0.0.10
+(August 10th, 2006)
+
+This is not a public release.
+
+
+* Removed bogus changelog.
+
+
+== Version 0.0.9
+(August 10th, 2006)
+
+This is not a public release.
+
+* Initial release.
+* Standard renderer.
+* Marker transformers: currency, percentages.
+* Basic Graphs: Area, Bar, Line.
+* Advanced Graphs: Average, AllSmiles.
+* Initial documentation.
20 License.txt
@@ -0,0 +1,20 @@
+Copyright (c) 2006 Brasten Sager (brasten@nagilum.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.
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2006 Brasten Sager (brasten@nagilum.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.
104 Manifest.txt
@@ -0,0 +1,104 @@
+CHANGES
+History.txt
+License.txt
+MIT-LICENSE
+Manifest.txt
+PostInstall.txt
+README
+README.txt
+Rakefile
+config/hoe.rb
+config/requirements.rb
+lib/scruffy.rb
+lib/scruffy/components.rb
+lib/scruffy/components/background.rb
+lib/scruffy/components/base.rb
+lib/scruffy/components/data_markers.rb
+lib/scruffy/components/graphs.rb
+lib/scruffy/components/grid.rb
+lib/scruffy/components/label.rb
+lib/scruffy/components/legend.rb
+lib/scruffy/components/style_info.rb
+lib/scruffy/components/title.rb
+lib/scruffy/components/value_markers.rb
+lib/scruffy/components/viewport.rb
+lib/scruffy/formatters.rb
+lib/scruffy/graph.rb
+lib/scruffy/graph_state.rb
+lib/scruffy/helpers.rb
+lib/scruffy/helpers/canvas.rb
+lib/scruffy/helpers/layer_container.rb
+lib/scruffy/helpers/meta.rb
+lib/scruffy/helpers/point_container.rb
+lib/scruffy/layers.rb
+lib/scruffy/layers/all_smiles.rb
+lib/scruffy/layers/area.rb
+lib/scruffy/layers/average.rb
+lib/scruffy/layers/bar.rb
+lib/scruffy/layers/base.rb
+lib/scruffy/layers/line.rb
+lib/scruffy/layers/pie.rb
+lib/scruffy/layers/pie_slice.rb
+lib/scruffy/layers/scatter.rb
+lib/scruffy/layers/sparkline_bar.rb
+lib/scruffy/layers/stacked.rb
+lib/scruffy/rasterizers.rb
+lib/scruffy/rasterizers/batik_rasterizer.rb
+lib/scruffy/rasterizers/rmagick_rasterizer.rb
+lib/scruffy/renderers.rb
+lib/scruffy/renderers/base.rb
+lib/scruffy/renderers/cubed.rb
+lib/scruffy/renderers/cubed3d.rb
+lib/scruffy/renderers/empty.rb
+lib/scruffy/renderers/pie.rb
+lib/scruffy/renderers/reversed.rb
+lib/scruffy/renderers/sparkline.rb
+lib/scruffy/renderers/split.rb
+lib/scruffy/renderers/standard.rb
+lib/scruffy/themes.rb
+lib/scruffy/version.rb
+script/console
+script/destroy
+script/generate
+script/txt2html
+setup.rb
+spec/scruffy/graph_spec.rb
+spec/scruffy/layers/base_spec.rb
+spec/scruffy/layers/line_spec.rb
+spec/spec_helper.rb
+tasks/deployment.rake
+tasks/environment.rake
+tasks/website.rake
+test/graph_creation_test.rb
+test/test_helper.rb
+website/images/blank.gif.html
+website/images/graphs/all_smiles.png
+website/images/graphs/bar_test.png
+website/images/graphs/bar_test.svg
+website/images/graphs/line_test.png
+website/images/graphs/line_test.svg
+website/images/graphs/multi_test.png
+website/images/graphs/multi_test.svg
+website/images/graphs/pie_test.png
+website/images/graphs/pie_test.svg
+website/images/graphs/split_test.png
+website/images/graphs/split_test.svg
+website/images/graphs/stacking_test.png
+website/images/graphs/stacking_test.svg
+website/images/header.png
+website/images/header_gradient.png
+website/images/overlay.png
+website/images/scruffy.png
+website/index.html
+website/index.txt
+website/javascripts/application.js
+website/javascripts/controls.js
+website/javascripts/dragdrop.js
+website/javascripts/effects.js
+website/javascripts/lightbox.js
+website/javascripts/prototype.js
+website/javascripts/rounded_corners_lite.inc.js
+website/stylesheets/lightbox.css
+website/stylesheets/screen.css
+website/stylesheets/scruffy.css
+website/template.html.erb
6 PostInstall.txt
@@ -0,0 +1,6 @@
+For more information on scruffy, see http://scruffy.rubyforge.org
+
+NOTE: Change this information in PostInstall.txt
+You can also delete it if you don't want it.
+
+
9 README
@@ -0,0 +1,9 @@
+==Scruffy Graphs
+
+Author:: Brasten Sager (brasten@nagilum.com)
+Date:: August 5th, 2006
+
+Scruffy is a Ruby library for generating high quality, good looking graphs. It is designed
+to be easy to use and highly customizable.
+
+For basic usage instructions, refer to the documentation for Scruffy::Graph.
66 README.txt
@@ -0,0 +1,66 @@
+= scruffy
+
+* scruffy.rubyforge.org
+
+Author:: Brasten Sager (brasten@nagilum.com)
+Date:: July 8, 2008
+
+== DESCRIPTION:
+
+Scruffy is a Ruby library for generating high quality, good looking graphs. It is designed
+to be easy to use and highly customizable.
+
+For basic usage instructions, refer to the documentation for Scruffy::Graph.
+
+== FEATURES
+
+* Renders to SVG or bitmap (PNG, JPG)
+
+== PROBLEMS:
+
+* 0.2.3 version has missing legend text when rendering to bitmap. This is strange because the text is there in the SVG before it goes to RMagick.
+
+== SYNOPSIS:
+
+ graph = Scruffy::Graph.new
+ graph.title = "Sample Line Graph"
+ graph.renderer = Scruffy::Renderers::Standard.new
+
+ graph.add :line, 'Example', [20, 100, 70, 30, 106]
+
+ graph.render :to => "line_test.svg"
+ graph.render :width => 300, :height => 200,
+ :to => "line_test.png", :as => 'png'
+
+== REQUIREMENTS:
+
+* Needs RMagick and Magic installed, if you wish to render to bitmap.
+
+== INSTALL:
+
+* sudo gem install scruffy
+
+== LICENSE:
+
+(The MIT License)
+
+Copyright (c) 2008 Brasten Sager (brasten@nagilum.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.
108 Rakefile
@@ -0,0 +1,108 @@
+require 'config/requirements'
+require 'config/hoe' # setup Hoe + all gem configuration
+
+Dir['tasks/**/*.rake'].each { |rake| load rake }
+
+#
+#require 'rubygems'
+#require 'rake'
+#require 'rake/testtask'
+#require 'rake/rdoctask'
+#require 'rake/packagetask'
+#require 'rake/gempackagetask'
+#require 'rake/contrib/rubyforgepublisher'
+#require 'lib/scruffy'
+#require 'spec'
+#require 'diff/lcs'
+#require 'spec/rake/spectask'
+#
+#PKG_NAME = "scruffy"
+#PKG_VERSION = Scruffy::VERSION::STRING
+#PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+#PKG_FILES = FileList[
+# '[A-Z]*',
+# 'lib/**/*.rb',
+# 'spec/**/*.rb'
+#]
+#
+#desc 'Runs all RSpec specs (set RCOV=true for coverage output)'
+#Spec::Rake::SpecTask.new do |t|
+# t.spec_files = FileList['spec/**/*_spec.rb']
+# t.rcov = ENV['RCOV']
+#end
+#
+#desc "Deploy docs to RubyForge"
+#task :rdoc_deploy => [:rdoc] do
+# dirs = %w{doc}
+# onserver = "brasten@rubyforge.org:/var/www/gforge-projects/scruffy"
+# dirs.each do | dir|
+# `scp -r "#{`pwd`.chomp}/#{dir}" "#{onserver}"`
+# end
+#end
+#
+## Genereate the RDoc documentation
+#Rake::RDocTask.new { |rdoc|
+# rdoc.rdoc_dir = 'doc'
+# rdoc.title = "Scruffy - Graphing Library for Ruby"
+## rdoc.options << '--line-numbers --inline-source --main README --accessor adv_attr_accessor=M'
+## rdoc.template = "#{ENV['template']}.rb" if ENV['template']
+# rdoc.rdoc_files.include('README', 'CHANGES', 'MIT-LICENSE')
+# rdoc.rdoc_files.include('lib/scruffy.rb')
+# rdoc.rdoc_files.include('lib/scruffy/*.rb')
+# rdoc.rdoc_files.include('lib/scruffy/layers/*.rb')
+# rdoc.rdoc_files.include('lib/scruffy/renderers/*.rb')
+# rdoc.rdoc_files.include('lib/scruffy/components/*.rb')
+# rdoc.rdoc_files.include('lib/scruffy/helpers/*.rb')
+# rdoc.rdoc_files.include('lib/scruffy/rasterizers/*.rb')
+#}
+#
+#spec = Gem::Specification.new do |s|
+# s.name = PKG_NAME
+# s.version = PKG_VERSION
+# s.author = AUTHOR
+# s.email = "brasten@nagilum.com"
+# s.homepage = "http://scruffy.rubyforge.org"
+# s.summary = "A powerful, clean graphing library for Ruby."
+# s.add_dependency('builder', '>= 2.0')
+# s.description = <<-EOF
+# Scruffy is a Ruby library for generating powerful graphs. It is based on
+# SVG, allowing for powerful, clean code, as well as a good foundation for
+# future features.
+# EOF
+#
+# s.files = PKG_FILES.to_a
+# s.has_rdoc = true
+# s.rubyforge_project = "scruffy"
+#end
+#
+#Rake::GemPackageTask.new(spec) do |pkg|
+# pkg.need_zip = true
+# pkg.need_tar = true
+#end
+#
+#task :verify_user do
+# raise "RUBYFORGE_USER environment variable not set!" unless ENV['RUBYFORGE_USER']
+#end
+#
+#task :verify_password do
+# raise "RUBYFORGE_PASSWORD environment variable not set!" unless ENV['RUBYFORGE_PASSWORD']
+#end
+#
+#desc "Publish gem+tgz+zip on RubyForge. You must make sure lib/version.rb is aligned with the CHANGELOG file"
+#task :publish_packages => [:verify_user, :verify_password, :package] do
+# require 'meta_project'
+# require 'rake/contrib/xforge'
+# release_files = FileList[
+# "pkg/#{PKG_FILE_NAME}.gem",
+# "pkg/#{PKG_FILE_NAME}.tgz",
+# "pkg/#{PKG_FILE_NAME}.zip"
+# ]
+#
+# Rake::XForge::Release.new(MetaProject::Project::XForge::RubyForge.new(PKG_NAME)) do |xf|
+# # Never hardcode user name and password in the Rakefile!
+# xf.user_name = ENV['RUBYFORGE_USER']
+# xf.password = ENV['RUBYFORGE_PASSWORD']
+# xf.files = release_files.to_a
+# xf.release_name = "Scruffy #{PKG_VERSION}"
+# end
+#end
78 config/hoe.rb
@@ -0,0 +1,78 @@
+require 'scruffy/version'
+
+AUTHOR = 'Brasten Sager' # can also be an array of Authors
+# , 'David Parry', 'A.J. Ostman', 'Mat Schaffer']
+# (Using array of authors doesn't work for some reason.
+
+EMAIL = ["brasten@nagilum.com", "david.parry@suranyami.com"]
+DESCRIPTION = "Scruffy is a Ruby library for generating high quality, good looking graphs. It is designed to be easy to use and highly customizable."
+SUMMARY = 'A powerful, clean graphing library for Ruby.'
+GEM_NAME = 'scruffy' # what ppl will type to install your gem
+RUBYFORGE_PROJECT = 'scruffy' # The unix name for your project
+HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
+DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
+EXTRA_DEPENDENCIES = [
+ ['builder', '>= 2.0'],
+ ['diff-lcs', '>= 1.1.2']
+] # An array of rubygem dependencies [name, version]
+
+@config_file = "~/.rubyforge/user-config.yml"
+@config = nil
+RUBYFORGE_USERNAME = "unknown"
+def rubyforge_username
+ unless @config
+ begin
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
+ rescue
+ puts <<-EOS
+ERROR: No rubyforge config file found: #{@config_file}
+Run 'rubyforge setup' to prepare your env for access to Rubyforge
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
+ EOS
+ exit
+ end
+ end
+ RUBYFORGE_USERNAME.replace @config["username"]
+end
+
+
+REV = nil
+# UNCOMMENT IF REQUIRED:
+# REV = YAML.load(`svn info`)['Revision']
+VERS = Scruffy::VERSION::STRING + (REV ? ".#{REV}" : "")
+RDOC_OPTS = ['--quiet', '--title', 'scruffy documentation',
+ "--opname", "index.html",
+ "--line-numbers",
+ "--main", "README",
+ "--inline-source"]
+
+class Hoe
+ def extra_deps
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
+ @extra_deps
+ end
+end
+
+# Generate all the Rake tasks
+# Run 'rake -T' to see list of generated tasks (from gem root directory)
+$hoe = Hoe.new(GEM_NAME, VERS) do |p|
+ p.developer(AUTHOR, EMAIL)
+ p.description = DESCRIPTION
+ p.summary = SUMMARY
+ p.url = HOMEPATH
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
+ p.test_globs = ["test/**/test_*.rb"]
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
+
+ # == Optional
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
+ #p.extra_deps = EXTRA_DEPENDENCIES
+
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
+ end
+
+CHANGES = $hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
+PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
+$hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
+$hoe.rsync_args = '-av --delete --ignore-errors'
+$hoe.spec.post_install_message = File.open(File.dirname(__FILE__) + "/../PostInstall.txt").read rescue ""
15 config/requirements.rb
@@ -0,0 +1,15 @@
+require 'fileutils'
+include FileUtils
+
+require 'rubygems'
+%w[rake hoe newgem rubigen].each do |req_gem|
+ begin
+ require req_gem
+ rescue LoadError
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
+ puts "Installation: gem install #{req_gem} -y"
+ exit
+ end
+end
+
+$:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
30 lib/scruffy.rb
@@ -0,0 +1,30 @@
+$:.unshift(File.dirname(__FILE__)) unless
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
+
+# ===Scruffy Graphing Library for Ruby
+#
+# Author:: Brasten Sager
+# Date:: August 5th, 2006
+#
+# For information on generating graphs using Scruffy, see the
+# documentation in Scruffy::Graph.
+#
+# For information on creating your own graph types, see the
+# documentation in Scruffy::Layers::Base.
+module Scruffy
+end
+
+require 'rubygems'
+gem 'builder', '>= 2.0'
+require 'builder'
+
+require 'scruffy/helpers'
+require 'scruffy/graph_state'
+require 'scruffy/graph'
+require 'scruffy/themes'
+require 'scruffy/version'
+require 'scruffy/formatters'
+require 'scruffy/rasterizers'
+require 'scruffy/layers'
+require 'scruffy/components'
+require 'scruffy/renderers'
21 lib/scruffy/components.rb
@@ -0,0 +1,21 @@
+# ===Scruffy Components
+#
+# Author:: Brasten Sager
+# Date:: August 16th, 2006
+#
+# Components make up the visual elements of a Scruffy graph.
+#
+# For examples, see Scruffy::Components::Base.
+module Scruffy::Components; end
+
+require 'scruffy/components/base'
+require 'scruffy/components/title'
+require 'scruffy/components/background'
+require 'scruffy/components/graphs'
+require 'scruffy/components/grid'
+require 'scruffy/components/value_markers'
+require 'scruffy/components/data_markers'
+require 'scruffy/components/legend'
+require 'scruffy/components/style_info'
+require 'scruffy/components/viewport'
+require 'scruffy/components/label'
24 lib/scruffy/components/background.rb
@@ -0,0 +1,24 @@
+module Scruffy
+ module Components
+ class Background < Base
+ def draw(svg, bounds, options={})
+ fill = "#EEEEEE"
+ case options[:theme].background
+ when Symbol, String
+ fill = options[:theme].background.to_s
+ when Array
+ fill = "url(#BackgroundGradient)"
+ svg.defs {
+ svg.linearGradient(:id=>'BackgroundGradient', :x1 => '0%', :y1 => '0%', :x2 => '0%', :y2 => '100%') {
+ svg.stop(:offset => '5%', 'stop-color' => options[:theme].background[0])
+ svg.stop(:offset => '95%', 'stop-color' => options[:theme].background[1])
+ }
+ }
+ end
+
+ # Render background (maybe)
+ svg.rect(:width => bounds[:width], :height => bounds[:height], :x => "0", :y => "0", :fill => fill) unless fill.nil?
+ end
+ end
+ end
+end
54 lib/scruffy/components/base.rb
@@ -0,0 +1,54 @@
+module Scruffy
+ module Components
+ # ===Scruffy::Components::Base
+ #
+ # Common attributes for all components, and a standard render method
+ # that calls draw after setting up the drawing transformations.
+ class Base
+ attr_reader :id
+
+ # In terms of percentages: [10, 10] == 10% by 10%
+ attr_accessor :position
+ attr_accessor :size
+ attr_accessor :options
+ attr_accessor :visible
+
+ def initialize(id, options = {})
+ @id = id.to_sym
+ @position = options[:position] || [0, 0]
+ @size = options[:size] || [100, 100]
+ @visible = options[:visible] || true
+ @options = options
+ end
+
+ def render(svg, bounds, options={})
+ if @visible
+ unless bounds.nil?
+ @render_height = bounds[:height]
+
+ svg.g(:id => id.to_s,
+ :transform => "translate(#{bounds.delete(:x)}, #{bounds.delete(:y)})") {
+
+ draw(svg, bounds, options.merge(@options))
+ }
+ else
+ process(svg, options.merge(@options))
+ end
+ end
+ end
+
+ def draw(svg, bounds, options={})
+ # Override this if visual component
+ end
+
+ def process(svg, options={})
+ # Override this NOT a visual component
+ end
+
+ protected
+ def relative(pct)
+ @render_height * ( pct / 100.to_f )
+ end
+ end
+ end
+end
26 lib/scruffy/components/data_markers.rb
@@ -0,0 +1,26 @@
+module Scruffy
+ module Components
+
+ class DataMarkers < Base
+
+ def draw(svg, bounds, options={})
+ unless options[:point_markers].nil?
+ point_distance = bounds[:width] / (options[:point_markers].size - 1).to_f
+
+ (0...options[:point_markers].size).map do |idx|
+ x_coord = point_distance * idx
+ svg.text(options[:point_markers][idx],
+ :x => x_coord,
+ :y => bounds[:height],
+ 'font-size' => relative(90),
+ 'font-family' => options[:theme].font_family,
+ :fill => (options[:theme].marker || 'white').to_s,
+ 'text-anchor' => 'middle') unless options[:point_markers][idx].nil?
+ end
+ end
+ end # draw
+
+ end # class
+
+ end
+end
48 lib/scruffy/components/graphs.rb
@@ -0,0 +1,48 @@
+module Scruffy
+ module Components
+
+ # Component for displaying Graphs layers.
+ #
+ # Is passed all graph layers from the Graph object.
+ #
+ # (This may change as the capability for Graph filtering and such fills out.)
+ class Graphs < Base
+ STACKED_OPACITY = 0.85;
+
+ def draw(svg, bounds, options={})
+ # If Graph is limited to a category, reject layers outside of it's scope.
+ applicable_layers = options[:layers].reject do |l|
+ if @options[:only]
+ (l.options[:category].nil? && l.options[:categories].nil?) ||
+ (!l.options[:category].nil? && l.options[:category] != @options[:only]) ||
+ (!l.options[:categories].nil? && !l.options[:categories].include?(@options[:only]))
+ else
+ false
+ end
+ end
+
+ applicable_layers.each_with_index do |layer, idx|
+ layer_options = {}
+ layer_options[:index] = idx
+ layer_options[:min_value] = options[:min_value]
+ layer_options[:max_value] = options[:max_value]
+ layer_options[:complexity] = options[:complexity]
+ layer_options[:size] = [bounds[:width], bounds[:height]]
+ layer_options[:color] = layer.preferred_color || layer.color || options[:theme].next_color
+ layer_options[:opacity] = opacity_for(idx)
+ layer_options[:theme] = options[:theme]
+
+ svg.g(:id => "component_#{id}_graph_#{idx}", :class => 'graph_layer') {
+ layer.render(svg, layer_options)
+ }
+ end # applicable_layers
+ end # draw
+
+ protected
+ def opacity_for(idx)
+ (idx == 0) ? 1.0 : (@options[:stacked_opacity] || STACKED_OPACITY)
+ end
+
+ end #class
+ end
+end
16 lib/scruffy/components/grid.rb
@@ -0,0 +1,16 @@
+module Scruffy
+ module Components
+ class Grid < Base
+ attr_accessor :markers
+
+ def draw(svg, bounds, options={})
+ markers = (options[:markers] || self.markers) || 5
+
+ (0...markers).each do |idx|
+ marker = ((1 / (markers - 1).to_f) * idx) * bounds[:height]
+ svg.line(:x1 => 0, :y1 => marker, :x2 => bounds[:width], :y2 => marker, :style => "stroke: #{options[:theme].marker.to_s}; stroke-width: 2;")
+ end
+ end
+ end
+ end
+end
17 lib/scruffy/components/label.rb
@@ -0,0 +1,17 @@
+module Scruffy
+ module Components
+ class Label < Base
+ def draw(svg, bounds, options={})
+ svg.text(@options[:text],
+ :class => 'text',
+ :x => (bounds[:width] / 2),
+ :y => bounds[:height],
+ 'font-size' => relative(100),
+ 'font-family' => options[:theme].font_family,
+ :fill => options[:theme].marker,
+ :stroke => 'none', 'stroke-width' => '0',
+ 'text-anchor' => (@options[:text_anchor] || 'middle'))
+ end
+ end
+ end
+end
105 lib/scruffy/components/legend.rb
@@ -0,0 +1,105 @@
+module Scruffy::Components
+
+ class Legend < Base
+ FONT_SIZE = 80
+
+ def draw(svg, bounds, options={})
+ vertical = options[:vertical_legend]
+ legend_info = relevant_legend_info(options[:layers])
+ @line_height, x, y, size = 0
+ if vertical
+ set_line_height = 0.08 * bounds[:height]
+ @line_height = bounds[:height] / legend_info.length
+ @line_height = set_line_height if @line_height >
+ set_line_height
+ else
+ set_line_height = 0.90 * bounds[:height]
+ @line_height = set_line_height
+ end
+
+ text_height = @line_height * FONT_SIZE / 100
+ # #TODO how does this related to @points?
+ active_width, points = layout(legend_info, vertical)
+
+ offset = (bounds[:width] - active_width) / 2 # Nudge over a bit for true centering
+
+ # Render Legend
+ points.each_with_index do |point, idx|
+ if vertical
+ x = 0
+ y = point
+ size = @line_height * 0.5
+ else
+ x = offset + point
+ y = 0
+ size = relative(50)
+ end
+
+ # "#{x} #{y} #{@line_height} #{size}"
+
+ svg.rect(:x => x,
+ :y => y,
+ :width => size,
+ :height => size,
+ :fill => legend_info[idx][:color])
+
+ svg.text(legend_info[idx][:title],
+ :x => x + @line_height,
+ :y => y + text_height * 0.75,
+ 'font-size' => text_height,
+ 'font-family' => options[:theme].font_family,
+ :style => "color: #{options[:theme].marker || 'white'}",
+ :fill => (options[:theme].marker || 'white'))
+ end
+ end # draw
+
+ protected
+ # Collects Legend Info from the provided Layers.
+ #
+ # Automatically filters by legend's categories.
+ def relevant_legend_info(layers, categories=(@options[:category] ? [@options[:category]] : @options[:categories]))
+ legend_info = layers.inject([]) do |arr, layer|
+ if categories.nil? ||
+ (categories.include?(layer.options[:category]) ||
+ (layer.options[:categories] && (categories & layer.options[:categories]).size > 0) )
+
+ data = layer.legend_data
+ arr << data if data.is_a?(Hash)
+ arr = arr + data if data.is_a?(Array)
+ end
+ arr
+ end
+ end # relevant_legend_info
+
+ # Returns an array consisting of the total width needed by the legend
+ # information, as well as an array of @x-coords for each element. If
+ # vertical, then these are @y-coords, and @x is 0
+ #
+ # ie: [200, [0, 50, 100, 150]]
+ def layout(legend_info_array, vertical = false)
+ if vertical
+ longest = 0
+ legend_info_array.each {|elem|
+ cur_length = relative(50) * elem[:title].length
+ longest = longest < cur_length ? cur_length : longest
+ }
+ y_positions = []
+ (0..legend_info_array.length - 1).each {|y|
+ y_positions << y * @line_height
+ }
+ [longest, y_positions]
+ else
+ legend_info_array.inject([0, []]) do |enum, elem|
+ enum[0] += (relative(50) * 2) if enum.first != 0 # Add spacer between elements
+ enum[1] << enum.first # Add location to points
+ enum[0] += relative(50) # Add room for color box
+ enum[0] += (relative(50) * elem[:title].length) # Add room for text
+
+ [enum.first, enum.last]
+ end
+ end
+ end
+
+ end # class Legend
+
+end # Scruffy::Components
22 lib/scruffy/components/style_info.rb
@@ -0,0 +1,22 @@
+module Scruffy
+ module Components
+ # Component used for adding CSS styling to SVG graphs.
+ #
+ # In hindsight, ImageMagick and Mozilla SVG's handling of CSS styling is
+ # extremely inconsistant, so use this at your own risk.
+ class StyleInfo < Base
+ def initialize(*args)
+ super
+
+ @visible = false
+ end
+ def process(svg, options={})
+ svg.defs {
+ svg.style(:type => "text/css") {
+ svg.cdata!("\n#{options[:selector]} {\n #{options[:style]}\n}\n")
+ }
+ }
+ end
+ end
+ end
+end
19 lib/scruffy/components/title.rb
@@ -0,0 +1,19 @@
+module Scruffy
+ module Components
+ class Title < Base
+ def draw(svg, bounds, options={})
+ if options[:title]
+ svg.text(options[:title],
+ :class => 'title',
+ :x => (bounds[:width] / 2),
+ :y => bounds[:height],
+ 'font-size' => relative(100),
+ 'font-family' => options[:theme].font_family,
+ :fill => options[:theme].marker,
+ :stroke => 'none', 'stroke-width' => '0',
+ 'text-anchor' => (@options[:text_anchor] || 'middle'))
+ end
+ end
+ end
+ end
+end
32 lib/scruffy/components/value_markers.rb
@@ -0,0 +1,32 @@
+module Scruffy
+ module Components
+ class ValueMarkers < Base
+ attr_accessor :markers
+
+ def draw(svg, bounds, options={})
+ markers = (options[:markers] || self.markers) || 5
+ all_values = []
+
+ (0...markers).each do |idx|
+ marker = ((1 / (markers - 1).to_f) * idx) * bounds[:height]
+ all_values << (options[:max_value] - options[:min_value]) * ((1 / (markers - 1).to_f) * idx) + options[:min_value]
+ end
+
+ (0...markers).each do |idx|
+ marker = ((1 / (markers - 1).to_f) * idx) * bounds[:height]
+ marker_value = (options[:max_value] - options[:min_value]) * ((1 / (markers - 1).to_f) * idx) + options[:min_value]
+ marker_value = options[:value_formatter].route_format(marker_value, idx, options.merge({:all_values => all_values})) if options[:value_formatter]
+
+ svg.text( marker_value.to_s,
+ :x => bounds[:width],
+ :y => (bounds[:height] - marker),
+ 'font-size' => relative(8),
+ 'font-family' => options[:theme].font_family,
+ :fill => ((options.delete(:marker_color_override) || options[:theme].marker) || 'white').to_s,
+ 'text-anchor' => 'end')
+ end
+
+ end
+ end
+ end
+end
37 lib/scruffy/components/viewport.rb
@@ -0,0 +1,37 @@
+module Scruffy::Components
+ # Component used to limit other visual components to a certain area on the graph.
+ class Viewport < Base
+ include Scruffy::Helpers::Canvas
+
+ def initialize(*args, &block)
+ super(*args)
+
+ self.components = []
+ block.call(self.components) if block
+ end
+
+ def draw(svg, bounds, options={})
+ svg.g(options_for) {
+ self.components.each do |component|
+ component.render(svg,
+ bounds_for([bounds[:width], bounds[:height]],
+ component.position,
+ component.size),
+ options)
+ end
+ }
+ end
+
+ private
+ def options_for
+ options = {}
+ %w(skewX skewY).each do |option|
+ if @options[option.to_sym]
+ options[:transform] ||= ''
+ options[:transform] = options[:transform] + "#{option.to_s}(#{@options[option.to_sym]})"
+ end
+ end
+ options
+ end
+ end
+end
195 lib/scruffy/formatters.rb
@@ -0,0 +1,195 @@
+# ===Scruffy Formatters
+#
+# Author:: Brasten Sager
+# Date:: August 16th, 2006
+#
+# Formatters are used to format the values displayed on the y-axis by
+# setting graph.value_formatter.
+#
+# Example:
+#
+# graph.value_formatter = Scruffy::Formatters::Currency.new(:precision => 0)
+#
+module Scruffy::Formatters
+
+ # == Scruffy::Formatters::Base
+ #
+ # Author:: Brasten Sager
+ # Date:: August 16th, 2006
+ #
+ # Formatters are used to format the values displayed on the y-axis by
+ # setting graph.value_formatter.
+ class Base
+
+ # Called by the value marker component. Routes the format call
+ # to one of a couple possible methods.
+ #
+ # If the formatter defines a #format method, the returned value is used
+ # as the value. If the formatter defines a #format! method, the value passed is
+ # expected to be modified, and is used as the value. (This may not actually work,
+ # in hindsight.)
+ def route_format(target, idx, options = {})
+ args = [target, idx, options]
+ if respond_to?(:format)
+ send :format, *args[0...self.method(:format).arity]
+ elsif respond_to?(:format!)
+ send :format!, *args[0...self.method(:format!).arity]
+ target
+ else
+ raise NameError, "Formatter subclass must container either a format() method or format!() method."
+ end
+ end
+
+ protected
+ def number_with_precision(number, precision=3) #:nodoc:
+ sprintf("%01.#{precision}f", number)
+ end
+ end
+
+ # Allows you to pass in a Proc for use as a formatter.
+ #
+ # Use:
+ #
+ # graph.value_formatter = Scruffy::Formatters::Custom.new { |value, idx, options| "Displays Returned Value" }
+ class Custom < Base
+ attr_reader :proc
+
+ def initialize(&block)
+ @proc = block
+ end
+
+ def format(target, idx, options)
+ proc.call(target, idx, options)
+ end
+ end
+
+
+
+ # Default number formatter.
+ # Limits precision, beautifies numbers.
+ class Number < Base
+ attr_accessor :precision, :separator, :delimiter, :precision_limit
+
+ # Returns a new Number formatter.
+ #
+ # Options:
+ # precision:: precision to use for value. Can be set to an integer, :none or :auto.
+ # :auto will use whatever precision is necessary to portray all the numerical
+ # information, up to :precision_limit.
+ #
+ # Example: [100.1, 100.44, 200.323] will result in [100.100, 100.440, 200.323]
+ #
+ # separator:: decimal separator. Defaults to '.'
+ # delimiter:: delimiter character. Defaults to ','
+ # precision_limit:: upper limit for auto precision.
+ def initialize(options = {})
+ @precision = options[:precision] || :none
+ @separator = options[:separator] || '.'
+ @delimiter = options[:delimiter] || ','
+ @precision_limit = options[:precision_limit] || 4
+ end
+
+ # Formats the value.
+ def format(target, idx, options)
+ my_precision = @precision
+
+ if @precision == :auto
+ my_precision = options[:all_values].inject(0) do |highest, current|
+ cur = current.to_f.to_s.split(".").last.size
+ cur > highest ? cur : highest
+ end
+
+ my_precision = @precision_limit if my_precision > @precision_limit
+ elsif @precision == :none
+ my_precision = 0
+ end
+
+ my_separator = @separator
+ my_separator = "" unless my_precision > 0
+ begin
+ parts = number_with_precision(target, my_precision).split('.')
+
+ number = parts[0].to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{@delimiter}") + my_separator + parts[1].to_s
+ number
+ rescue StandardError => e
+ target
+ end
+ end
+ end
+
+ # Currency formatter.
+ #
+ # Provides formatting for currencies.
+ class Currency < Base
+
+ # Returns a new Currency class.
+ #
+ # Options:
+ # precision:: precision of value
+ # unit:: Defaults to '$'
+ # separator:: Defaults to '.'
+ # delimiter:: Defaults to ','
+ # negative_color:: Color of value marker for negative values. Defaults to 'red'
+ # special_negatives:: If set to true, parenthesizes negative numbers. ie: -$150.50 becomes ($150.50).
+ # Defaults to false.
+ def initialize(options = {})
+ @precision = options[:precision] || 2
+ @unit = options[:unit] || '$'
+ @separator = options[:separator] || '.'
+ @delimiter = options[:delimiter] || ','
+ @negative_color = options[:negative_color] || 'red'
+ @special_negatives = options[:special_negatives] || false
+ end
+
+ # Formats value marker.
+ def format(target, idx, options)
+ @separator = "" unless @precision > 0
+ begin
+ parts = number_with_precision(target, @precision).split('.')
+ if @special_negatives && (target.to_f < 0)
+ number = "(" + @unit + parts[0].to_i.abs.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{@delimiter}") + @separator + parts[1].to_s + ")"
+ else
+ number = @unit + parts[0].to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{@delimiter}") + @separator + parts[1].to_s
+ end
+ if (target.to_f < 0) && @negative_color
+ options[:marker_color_override] = @negative_color
+ end
+ number
+ rescue
+ target
+ end
+ end
+ end
+
+ # Percentage formatter.
+ #
+ # Provides formatting for percentages.
+ class Percentage < Base
+
+ # Returns new Percentage formatter.
+ #
+ # Options:
+ # precision:: Defaults to 3.
+ # separator:: Defaults to '.'
+ def initialize(options = {})
+ @precision = options[:precision] || 3
+ @separator = options[:separator] || '.'
+ end
+
+ # Formats percentages.
+ def format(target)
+ begin
+ number = number_with_precision(target, @precision)
+ parts = number.split('.')
+ if parts.at(1).nil?
+ parts[0] + "%"
+ else
+ parts[0] + @separator + parts[1].to_s + "%"
+ end
+ rescue
+ target
+ end
+ end
+ end
+
+end
189 lib/scruffy/graph.rb
@@ -0,0 +1,189 @@
+require 'forwardable'
+
+module Scruffy
+
+ # ==Scruffy Graphs
+ #
+ # Author:: Brasten Sager
+ # Date:: August 5th, 2006
+ #
+ #
+ # ====Graphs vs. Layers (Graph Types)
+ #
+ # Scruffy::Graph is the primary class you will use to generate your graphs. A Graph does not
+ # define a graph type nor does it directly hold any data. Instead, a Graph object can be thought
+ # of as a canvas on which other graphs are draw. (The actual graphs themselves are subclasses of Scruffy::Layers::Base)
+ # Despite the technical distinction, we will refer to Scruffy::Graph objects as 'graphs' and Scruffy::Layers as
+ # 'layers' or 'graph types.'
+ #
+ #
+ # ==== Creating a Graph
+ #
+ # You can begin building a graph by instantiating a Graph object and optionally passing a hash
+ # of properties.
+ #
+ # graph = Scruffy::Graph.new
+ #
+ # OR
+ #
+ # graph = Scruffy::Graph.new(:title => "Monthly Profits", :theme => Scruffy::Themes::RubyBlog.new)
+ #
+ # Once you have a Graph object, you can set any Graph-level properties (title, theme, etc), or begin adding
+ # graph layers. You can add a graph layer to a graph by using the Graph#add or Graph#<< methods. The two
+ # methods are identical and used to accommodate syntax preferences.
+ #
+ # graph.add(:line, 'John', [100, -20, 30, 60])
+ # graph.add(:line, 'Sara', [120, 50, -80, 20])
+ #
+ # OR
+ #
+ # graph << Scruffy::Layers::Line.new(:title => 'John', :points => [100, -20, 30, 60])
+ # graph << Scruffy::Layers::Line.new(:title => 'Sara', :points => [120, 50, -80, 20])
+ #
+ # Now that we've created our graph and added a layer to it, we're ready to render! You can render the graph
+ # directly to SVG or any other image format (supported by RMagick) with the Graph#render method:
+ #
+ # graph.render # Renders a 600x400 SVG graph
+ #
+ # OR
+ #
+ # graph.render(:width => 1200)
+ #
+ # # For image formats other than SVG:
+ # graph.render(:width => 1200, :as => 'PNG')
+ #
+ # # To render directly to a file:
+ # graph.render(:width => 5000, :to => '<filename>')
+ #
+ # graph.render(:width => 700, :as => 'PNG', :to => '<filename>')
+ #
+ # And that's your basic Scruffy graph! Please check the documentation for the various methods and
+ # classes you'll be using, as there are a bunch of options not demonstrated here.
+ #
+ # A couple final things worth noting:
+ # * You can call Graph#render as often as you wish with different rendering options. In
+ # fact, you can modify the graph any way you wish between renders.
+ #
+ #
+ # * There are no restrictions to the combination of graph layers you can add. It is perfectly
+ # valid to do something like:
+ # graph.add(:line, [100, 200, 300])
+ # graph.add(:bar, [200, 150, 150])
+ #
+ # Of course, while you may be able to combine some things such as pie charts and line graphs, that
+ # doesn't necessarily mean they will make any logical sense together. We leave those decisions up to you. :)
+
+ class Graph
+ extend Forwardable;
+
+ include Scruffy::Helpers::LayerContainer
+
+ # Delegating these getters to the internal state object.
+ def_delegators :internal_state, :title, :theme, :default_type,
+ :point_markers, :value_formatter, :rasterizer
+
+ def_delegators :internal_state, :title=, :theme=, :default_type=,
+ :point_markers=, :value_formatter=, :rasterizer=
+
+ attr_reader :renderer # Writer defined below
+
+ # Returns a new Graph. You can optionally pass in a default graph type and an options hash.
+ #
+ # Graph.new # New graph
+ # Graph.new(:line) # New graph with default graph type of Line
+ # Graph.new({...}) # New graph with options.
+ #
+ # Options:
+ #
+ # title:: Graph's title
+ # theme:: A theme object to use when rendering graph
+ # layers:: An array of Layers for this graph to use
+ # default_type:: A symbol indicating the default type of Layer for this graph
+ # value_formatter:: Sets a formatter used to modify marker values prior to rendering
+ # point_markers:: Sets the x-axis marker values
+ # rasterizer:: Sets the rasterizer to use when rendering to an image format. Defaults to RMagick.
+ def initialize(*args)
+ self.default_type = args.shift if args.first.is_a?(Symbol)
+ options = args.shift.dup if args.first.is_a?(Hash)
+ raise ArgumentError, "The arguments provided are not supported." if args.size > 0
+
+ options ||= {}
+ self.theme = Scruffy::Themes::Keynote.new
+ self.renderer = Scruffy::Renderers::Standard.new
+ self.rasterizer = Scruffy::Rasterizers::RMagickRasterizer.new
+ self.value_formatter = Scruffy::Formatters::Number.new
+
+ %w(title theme layers default_type value_formatter point_markers rasterizer).each do |arg|
+ self.send("#{arg}=".to_sym, options.delete(arg.to_sym)) unless options[arg.to_sym].nil?
+ end
+
+ raise ArgumentError, "Some options provided are not supported: #{options.keys.join(' ')}." if options.size > 0
+ end
+
+ # Renders the graph in it's current state to an SVG object.
+ #
+ # Options:
+ # size:: An array indicating the size you wish to render the graph. ( [x, y] )
+ # width:: The width of the rendered graph. A height is calculated at 3/4th of the width.
+ # theme:: Theme used to render graph for this render only.
+ # min_value:: Overrides the calculated minimum value used for the graph.
+ # max_value:: Overrides the calculated maximum value used for the graph.
+ #
+ # For other image formats:
+ # as:: File format to render to ('PNG', 'JPG', etc)
+ # to:: Name of file to save graph to, if desired. If not provided, image is returned as blob/string.
+ def render(options = {})
+ options[:theme] ||= theme
+ options[:value_formatter] ||= value_formatter
+ options[:point_markers] ||= point_markers
+ options[:size] ||= (options[:width] ? [options[:width], (options.delete(:width) * 0.6).to_i] : [600, 360])
+ options[:title] ||= title
+ options[:layers] ||= layers
+ options[:min_value] ||= bottom_value(:padded)
+ options[:max_value] ||= top_value
+ options[:graph] ||= self
+
+
+ # Removed for now.
+ # Added for making smaller fonts more legible, but may not be needed after all.
+ #
+ # if options[:as] && (options[:size][0] <= 300 || options[:size][1] <= 200)
+ # options[:actual_size] = options[:size]
+ # options[:size] = [800, (800.to_f * (options[:actual_size][1].to_f / options[:actual_size][0].to_f))]
+ # end
+
+ svg = ( options[:renderer].nil? ? self.renderer.render( options ) : options[:renderer].render( options ) )
+
+ # SVG to file.
+ if options[:to] && options[:as].nil?
+ File.open(options[:to], 'w') { |file|
+ file.write(svg)
+ }
+ end
+
+ options[:as] ? rasterizer.rasterize(svg, options) : svg
+ end
+
+ def renderer=(val)
+ raise ArgumentError, "Renderer must include a #render(options) method." unless (val.respond_to?(:render) && val.method(:render).arity.abs > 0)
+
+ @renderer = val
+ end
+
+ alias :layout :renderer
+
+ def component(id)
+ renderer.component(id)
+ end
+
+ def remove(id)
+ renderer.remove(id)
+ end
+
+ private
+ def internal_state
+ @internal_state ||= GraphState.new
+ end
+
+ end
+end
24 lib/scruffy/graph_state.rb
@@ -0,0 +1,24 @@
+# ===GraphState
+#
+# Author:: Brasten Sager
+# Date:: September 27th, 2007
+#
+# State object for holding all of the graph's
+# settings. Attempting to clean up the
+# graph interface a bit.
+
+module Scruffy
+ class GraphState
+
+ attr_accessor :title
+ attr_accessor :theme
+ attr_accessor :default_type
+ attr_accessor :point_markers
+ attr_accessor :value_formatter
+ attr_accessor :rasterizer
+
+ def initialize
+
+ end
+ end
+end
12 lib/scruffy/helpers.rb
@@ -0,0 +1,12 @@
+# ==Scruffy Helpers
+#
+# Author:: Brasten Sager
+# Date:: August 16th, 2006
+#
+# Modules which provide helper methods for various situations.
+module Scruffy::Helpers; end
+
+require 'scruffy/helpers/canvas'
+require 'scruffy/helpers/layer_container'
+require 'scruffy/helpers/point_container'
+#require 'scruffy/helpers/meta'
41 lib/scruffy/helpers/canvas.rb
@@ -0,0 +1,41 @@
+module Scruffy::Helpers
+
+ # ==Scruffy::Helpers::Canvas
+ #
+ # Author:: Brasten Sager
+ # Date:: August 16th, 2006
+ #
+ # Provides common methods for canvas objects. Primarily used for providing
+ # spacial-type calculations where necessary.
+ module Canvas
+ attr_accessor :components
+
+ def reset_settings!
+ self.options = {}
+ end
+
+ def component(id, components=self.components)
+ components.find {|elem| elem.id == id}
+ end
+
+ def remove(id, components=self.components)
+ components.delete(component(id))
+ end
+
+ protected
+ # Converts percentage values into actual pixel values based on the known
+ # render size.
+ #
+ # Returns a hash consisting of :x, :y, :width, and :height elements.
+ def bounds_for(canvas_size, position, size)
+ return nil if (position.nil? || size.nil?)
+ bounds = {}
+ bounds[:x] = canvas_size.first * (position.first / 100.to_f)
+ bounds[:y] = canvas_size.last * (position.last / 100.to_f)
+ bounds[:width] = canvas_size.first * (size.first / 100.to_f)
+ bounds[:height] = canvas_size.last * (size.last / 100.to_f)
+ bounds
+ end
+ end # canvas
+
+end # scruffy::helpers
95 lib/scruffy/helpers/layer_container.rb
@@ -0,0 +1,95 @@
+module Scruffy::Helpers
+
+ # ==Scruffy::Helpers::LayerContainer
+ #
+ # Author:: Brasten Sager
+ # Date:: August 16th, 2006
+ #
+ # Adds some common functionality to any object which needs to act as a
+ # container for graph layers. The best example of this is the Scruffy::Graph
+ # object itself, but this module is also used by Scruffy::Layer::Stacked.
+ module LayerContainer
+
+ # Adds a Layer to the Graph/Container. Accepts either a list of
+ # arguments used to build a new layer, or a Scruffy::Layers::Base-derived
+ # object. When passing a list of arguments, all arguments are optional,
+ # but the arguments specified must be provided in a particular order:
+ # type (Symbol), title (String), points (Array), options (Hash).
+ #
+ # Both #add and #<< can be used.
+ #
+ # graph.add(:line, [100, 200, 150]) # Create and add an untitled line graph
+ #
+ # graph << (:line, "John's Sales", [150, 100]) # Create and add a titled line graph
+ #
+ # graph << Scruffy::Layers::Bar.new({...}) # Adds Bar layer to graph
+ #
+ def <<(*args, &block)
+ if args[0].kind_of?(Scruffy::Layers::Base)
+ layers << args[0]
+ else
+ type = args.first.is_a?(Symbol) ? args.shift : @default_type
+ title = args.shift if args.first.is_a?(String)
+
+ # Layer handles PointContainer mixin, don't do it here
+ points = [Array, Hash].include?(args.first.class) ? args.shift : []
+ options = args.first.is_a?(Hash) ? args.shift : {}
+
+ title ||= ''
+
+ raise ArgumentError,
+ 'You must specify a graph type (:area, :bar, :line, etc) if you do not have a default type specified.' if type.nil?
+
+ class_name = "Scruffy::Layers::#{to_camelcase(type.to_s)}"
+ layer_class = Kernel::module_eval(class_name)
+ options = {:points => points, :title => title}.merge options
+ layer = layer_class.new(options, &block)
+ layers << layer
+ end
+ layer
+ end
+
+ alias :add :<<
+
+
+ # Layer Writer
+ def layers=(val)
+ @layers = val
+ end
+
+ # Layer Reader
+ def layers
+ @layers ||= []
+ end
+
+ # Returns the highest value in any of this container's layers.
+ #
+ # If padding is set to :padded, a 15% padding is added to the highest value.
+ def top_value(padding=nil) # :nodoc:
+ topval = layers.inject(0) { |max, layer| (max = ((max < layer.top_value) ? layer.top_value : max)) unless layer.top_value.nil?; max }
+ padding == :padded ? (topval - ((topval - bottom_value) * 0.15)) : topval
+ end
+
+ # Returns the lowest value in any of this container's layers.
+ #
+ # If padding is set to :padded, a 15% padding is added below the lowest value.
+ # If the lowest value is greater than zero, then the padding will not cross the zero line, preventing
+ # negative values from being introduced into the graph purely due to padding.
+ def bottom_value(padding=nil) # :nodoc:
+ botval = layers.inject(top_value) { |min, layer| (min = ((min > layer.bottom_value) ? layer.bottom_value : min)) unless layer.bottom_value.nil?; min }
+ above_zero = (botval > 0)
+ botval = (botval - ((top_value - botval) * 0.15))
+
+ # Don't introduce negative values solely due to padding.
+ # A user-provided value must be negative before padding will extend into negative values.
+ (above_zero && botval < 0) ? 0 : botval
+ end
+
+
+ protected
+ def to_camelcase(type) # :nodoc:
+ type.split('_').map { |e| e.capitalize }.join('')
+ end
+
+ end
+end
5 lib/scruffy/helpers/meta.rb
@@ -0,0 +1,5 @@
+# module Scruffy::Helpers::MetaAttributes
+# def singleton_class
+# (class << self; self; end)
+# end
+# end
70 lib/scruffy/helpers/point_container.rb
@@ -0,0 +1,70 @@
+module Scruffy::Helpers
+
+ # ==Scruffy::Helpers::PointContainer
+ #
+ # Author:: Mat Schaffer
+ # Date:: March 22nd, 2007
+ #
+ # Allows all standard point operations to be called on both Array and Hash
+ module PointContainer
+ def self.extended point_set
+ point_set.extend(const_get(point_set.class.to_s))
+ end
+
+ def sortable_values
+ values.find_all { |v| v.respond_to? :<=> }
+ end
+
+ def summable_values
+ values.find_all { |v| v.respond_to? :+ }
+ end
+
+ def maximum_value
+ sortable_values.sort.last
+ end
+
+ def minimum_value
+ sortable_values.sort.first
+ end
+
+ def sum
+ summable_values.inject(0) { |sum, i| sum += i }
+ end
+
+ def inject_with_index memo
+ index = 0
+ inject(memo) do |memo, item|
+ ret = yield memo, item, index
+ index = index.succ
+ ret
+ end
+ end
+
+ module Array
+ def values
+ self
+ end
+ end
+
+ module Hash
+ def minimum_key
+ self.keys.sort.first
+ end
+
+ def maximum_key
+ self.keys.sort.last
+ end
+
+ def inject memo
+ (minimum_key..maximum_key).each do |i|
+ memo = yield memo, self[i]
+ end
+ memo
+ end
+
+ def size
+ maximum_key - minimum_key + 1
+ end
+ end
+ end
+end
24 lib/scruffy/layers.rb
@@ -0,0 +1,24 @@
+# ==Scruffy::Layers
+#
+# Author:: Brasten Sager
+# Date:: August 10th, 2006
+#
+# See documentation in Scruffy::Layers::Base
+#
+module Scruffy::Layers
+
+ # Should be raised whenever a predictable error during rendering occurs,
+ # particularly if you do not want to terminate the graph rendering process.
+ class RenderError < StandardError; end
+
+end
+
+require 'scruffy/layers/base'
+require 'scruffy/layers/area'
+require 'scruffy/layers/all_smiles'
+require 'scruffy/layers/bar'
+require 'scruffy/layers/line'
+require 'scruffy/layers/average'
+require 'scruffy/layers/stacked'
+require 'scruffy/layers/pie'
+require 'scruffy/layers/pie_slice'
137 lib/scruffy/layers/all_smiles.rb
@@ -0,0 +1,137 @@
+module Scruffy::Layers
+ # ==Scruffy::Layers::AllSmiles
+ #
+ # Author:: Brasten Sager
+ # Date:: August 8th, 2006
+ #
+ # The AllSmiles graph consists of smiley faces for data points, with smiles or frowns depending upon
+ # their relative location on the graph. The highest point is crowned with a wizard hat. The Wizard
+ # Smiley eventually become 'Scruffy', our mascot.
+ #
+ # I don't know why.
+ #
+ # This graph only looks decent in SVG mode. If you're rasterizing the graph with ImageMagick, you
+ # must use the :complexity => :minimal option on Graph#render. This will make the graph look really
+ # nasty, but still better than if you try to rasterize with all the gradients in place.
+ class AllSmiles < Base
+ attr_accessor :standalone
+
+ # Returns a new AllSmiles graph.
+ #
+ # Options:
+ # standalone:: If set to true, dashed lines under smilies run vertically, like bar graphs.
+ # If false (default), dashed lines run from smiley to smiley, like a line-graph.
+ def initialize(options = {})
+ super
+ @standalone = options[:standalone] || false
+ end
+
+ # Renders graph.
+ def draw(svg, coords, options={})
+
+ hero_smiley = nil
+ coords.each { |c| hero_smiley = c.last if (hero_smiley.nil? || c.last < hero_smiley) }
+
+ svg.defs {
+ svg.radialGradient(:id => 'SmileyGradient', :cx => '50%',
+ :cy => '50%', :r => '50%', :fx => '30%', :fy => '30%') {
+
+ svg.stop(:offset => '0%', 'stop-color' => '#FFF')
+ svg.stop(:offset => '20%', 'stop-color' => '#FFC')
+ svg.stop(:offset => '45%', 'stop-color' => '#FF3')
+ svg.stop(:offset => '60%', 'stop-color' => '#FF0')
+ svg.stop(:offset => '90%', 'stop-color' => '#990')
+ svg.stop(:offset => '100%', 'stop-color' => '#220')
+ }
+ svg.radialGradient(:id => 'HeroGradient', :cx => '50%',
+ :cy => '50%', :r => '50%', :fx => '30%', :fy => '30%') {
+
+ svg.stop(:offset => '0%', 'stop-color' => '#FEE')
+ svg.stop(:offset => '20%', 'stop-color' => '#F0E0C0')
+ svg.stop(:offset => '45%', 'stop-color' => '#8A2A1A')
+ svg.stop(:offset => '60%', 'stop-color' => '#821')
+ svg.stop(:offset => '90%', 'stop-color' => '#210')
+ }
+ svg.radialGradient(:id => 'StarGradient', :cx => '50%',
+ :cy => '50%', :r => '50%', :fx => '30%', :fy => '30%') {
+
+ svg.stop(:offset => '0%', 'stop-color' => '#FFF')
+ svg.stop(:offset => '20%', 'stop-color' => '#EFEFEF')
+ svg.stop(:offset => '45%', 'stop-color' => '#DDD')
+ svg.stop(:offset => '60%', 'stop-color' => '#BBB')
+ svg.stop(:offset => '90%', 'stop-color' => '#888')
+ }
+ }
+
+ unless standalone
+ svg.polyline( :points => stringify_coords(coords).join(' '), :fill => 'none',
+ :stroke => '#660', 'stroke-width' => scaled(10), 'stroke-dasharray' => "#{scaled(10)}, #{scaled(10)}" )
+ end
+
+ # Draw smilies.
+ coords.each do |coord|
+ if standalone
+ svg.line( :x1 => coord.first, :y1 => coord.last, :x2 => coord.first, :y2 => height, :fill => 'none',
+ :stroke => '#660', 'stroke-width' => scaled(10), 'stroke-dasharray' => "#{scaled(10)}, #{scaled(10)}" )
+ end
+ svg.circle( :cx => coord.first + scaled(2), :cy => coord.last + scaled(2), :r => scaled(15),
+ :fill => 'black', :stroke => 'none', :opacity => 0.4)
+ svg.circle( :cx => coord.first, :cy => coord.last, :r => scaled(15),
+ :fill => (complexity == :minimal ? 'yellow' : 'url(#SmileyGradient)'), :stroke => 'black', 'stroke-width' => scaled(1) )
+ svg.line( :x1 => (coord.first - scaled(3)),
+ :x2 => (coord.first - scaled(3)),
+ :y1 => (coord.last),
+ :y2 => (coord.last - scaled(7)), :stroke => 'black', 'stroke-width' => scaled(1.4) )
+ svg.line( :x1 => (coord.first + scaled(3)),
+ :x2 => (coord.first + scaled(3)),
+ :y1 => (coord.last),
+ :y2 => (coord.last - scaled(7)), :stroke => 'black', 'stroke-width' => scaled(1.4) )
+
+
+ # Some minor mathematics for the smile/frown
+ percent = 1.0 - (coord.last.to_f / height.to_f)
+ corners = scaled(8 - (5 * percent))
+ anchor = scaled((20 * percent) - 5)
+
+ # Draw the mouth
+ svg.path( :d => "M#{coord.first - scaled(9)} #{coord.last + corners} Q#{coord.first} #{coord.last + anchor} #{coord.first + scaled(9)} #{coord.last + corners}",
+ :stroke => 'black', 'stroke-width' => scaled(1.4), :fill => 'none' )
+
+
+ # Wizard hat for hero smiley.
+ if coord.last == hero_smiley
+ svg.ellipse(:cx => coord.first, :cy => (coord.last - scaled(13)),
+ :rx => scaled(17), :ry => scaled(6.5), :fill => (complexity == :minimal ? 'purple' : 'url(#HeroGradient)'), :stroke => 'black', 'stroke-width' => scaled(1.4) )
+
+ svg.path(:d => "M#{coord.first} #{coord.last - scaled(60)} " +
+ "L#{coord.first + scaled(10)} #{coord.last - scaled(14)} " +
+ "C#{coord.first + scaled(10)},#{coord.last - scaled(9)} #{coord.first - scaled(10)},#{coord.last - scaled(9)} #{coord.first - scaled(10)},#{coord.last - scaled(14)}" +
+ "L#{coord.first} #{coord.last - scaled(60)}",
+ :stroke => 'black', 'stroke-width' => scaled(1.4), :fill => (complexity == :minimal ? 'purple' : 'url(#HeroGradient)'))
+
+ svg.path(:d => "M#{coord.first - scaled(4)} #{coord.last - scaled(23)}" +
+ "l-#{scaled(2.5)} #{scaled(10)} l#{scaled(7.5)} -#{scaled(5)} l-#{scaled(10)} 0 l#{scaled(7.5)} #{scaled(5)} l-#{scaled(2.5)} -#{scaled(10)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
+ svg.path(:d => "M#{coord.first + scaled(2)} #{coord.last - scaled(30)}" +
+ "l-#{scaled(2.5)} #{scaled(10)} l#{scaled(7.5)} -#{scaled(5)} l-#{scaled(10)} 0 l#{scaled(7.5)} #{scaled(5)} l-#{scaled(2.5)} -#{scaled(10)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
+ svg.path(:d => "M#{coord.first - scaled(2)} #{coord.last - scaled(33)}" +
+ "l-#{scaled(1.25)} #{scaled(5)} l#{scaled(3.75)} -#{scaled(2.5)} l-#{scaled(5)} 0 l#{scaled(3.75)} #{scaled(2.5)} l-#{scaled(1.25)} -#{scaled(5)}", :stroke => 'none', :fill => 'white' )
+ svg.path(:d => "M#{coord.first - scaled(2.2)} #{coord.last - scaled(32.7)}" +
+ "l-#{scaled(1.25)} #{scaled(5)} l#{scaled(3.75)} -#{scaled(2.5)} l-#{scaled(5)} 0 l#{scaled(3.75)} #{scaled(2.5)} l-#{scaled(1.25)} -#{scaled(5)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
+ svg.path(:d => "M#{coord.first + scaled(4.5)} #{coord.last - scaled(20)}" +
+ "l-#{scaled(1.25)} #{scaled(5)} l#{scaled(3.75)} -#{scaled(2.5)} l-#{scaled(5)} 0 l#{scaled(3.75)} #{scaled(2.5)} l-#{scaled(1.25)} -#{scaled(5)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
+ svg.path(:d => "M#{coord.first} #{coord.last - scaled(40)}" +
+ "l-#{scaled(1.25)} #{scaled(5)} l#{scaled(3.75)} -#{scaled(2.5)} l-#{scaled(5)} 0 l#{scaled(3.75)} #{scaled(2.5)} l-#{scaled(1.25)} -#{scaled(5)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
+
+ end
+
+ end
+ end
+
+ # Legacy (4 days old). Removed scaled from layout engine,
+ # changed to #relative, with different math involved.
+ # Translate here so I don't have to entirely redo this graph.
+ def scaled(pt)
+ relative(pt) / 2
+ end
+ end
+end
46 lib/scruffy/layers/area.rb
@@ -0,0 +1,46 @@
+module Scruffy::Layers
+ # ==Scruffy::Layers::Area
+ #
+ # Author:: Brasten Sager
+ # Date:: August 6th, 2006
+ #
+ # Standard area graph.
+ class Area < Base
+
+ # Render area graph.
+ def draw(svg, coords, options={})
+ # svg.polygon wants a long string of coords.
+ points_value = "0,#{height} #{stringify_coords(coords).join(' ')} #{width},#{height}"
+
+ # Experimental, for later user.
+ # This was supposed to add some fun filters, 3d effects and whatnot.
+ # Neither ImageMagick nor Mozilla SVG render this well (at all). Maybe a future thing.
+ #
+ # svg.defs {
+ # svg.filter(:id => 'MyFilter', :filterUnits => 'userSpaceOnUse', :x => 0, :y => 0, :width => 200, :height => '120') {
+ # svg.feGaussianBlur(:in => 'SourceAlpha', :stdDeviation => 4, :result => 'blur')
+ # svg.feOffset(:in => 'blur', :dx => 4, :dy => 4, :result => 'offsetBlur')
+ # svg.feSpecularLighting( :in => 'blur', :surfaceScale => 5, :specularConstant => '.75',
+ # :specularExponent => 20, 'lighting-color' => '#bbbbbb',
+ # :result => 'specOut') {
+ # svg.fePointLight(:x => '-5000', :y => '-10000', :z => '20000')
+ # }
+ #
+ # svg.feComposite(:in => 'specOut', :in2 => 'SourceAlpha', :operator => 'in', :result => 'specOut')
+ # svg.feComposite(:in => 'sourceGraphic', :in2 => 'specOut', :operator => 'arithmetic',
+ # :k1 => 0, :k2 => 1, :k3 => 1, :k4 => 0, :result => 'litPaint')
+ #
+ # svg.feMerge {
+ # svg.feMergeNode(:in => 'offsetBlur')
+ # svg.feMergeNode(:in => 'litPaint')
+ # }
+ # }
+ # }
+ svg.g(:transform => "translate(0, -#{relative(2)})") {
+ svg.polygon(:points => points_value, :style => "fill: black; stroke: black; fill-opacity: 0.06; stroke-opacity: 0.06;")
+ }
+
+ svg.polygon(:points => points_value, :fill => color.to_s, :stroke => color.to_s, 'style' => "opacity: #{opacity}")
+ end
+ end
+end
67 lib/scruffy/layers/average.rb
@@ -0,0 +1,67 @@
+module Scruffy::Layers
+ # ==Scruffy::Layers::Average
+ #
+ # Author:: Brasten Sager
+ # Date:: August 7th, 2006
+ #
+ # An 'average' graph. This graph iterates through all the layers and averages
+ # all the data at each point, then draws a thick, translucent, shadowy line graph
+ # indicating the average values.
+ #
+ # This only looks decent in SVG mode. ImageMagick doesn't retain the transparency
+ # for some reason, creating a massive black line. Any help resolving this would
+ # be useful.
+ class Average < Base
+ attr_reader :layers
+
+ # Returns new Average graph.
+ def initialize(options = {})
+ # Set self's relevant_data to false. Otherwise we get stuck in a
+ # recursive loop.
+ super(options.merge({:relevant_data => false}))
+
+ # The usual :points argument is actually layers for Average, name it as such
+ @layers = options[:points]
+ end
+
+ # Render average graph.
+ def draw(svg, coords, options = {})
+ svg.polyline( :points => coords.join(' '), :fill => 'none', :stroke => 'black',
+ 'stroke-width' => relative(5), 'opacity' => '0.4')
+ end
+
+ protected
+ # Override default generate_coordinates method to iterate through the layers and
+ # generate coordinates based on the average data points.
+ def generate_coordinates(options = {})
+ key_layer = layers.find { |layer| layer.relevant_data? }
+
+ options[:point_distance] = width / (key_layer.points.size - 1).to_f
+
+ coords = []
+
+ #TODO this will likely break with the new hash model
+ key_layer.points.each_with_index do |layer, idx|
+ sum, objects = points.inject([0, 0]) do |arr, elem|
+ if elem.relevant_data?
+ arr[0] += elem.points[idx]
+ arr[1] += 1
+ end
+ arr
+ end
+
+ average = sum / objects.to_f
+
+ x_coord = options[:point_distance] * idx
+
+ relative_percent = ((average == min_value) ? 0 : ((average - min_value) / (max_value - min_value).to_f))
+ y_coord = (height - (height * relative_percent))
+
+ coords << [x_coord, y_coord].join(',')
+ end
+
+ return coords
+ end
+
+ end
+end
52 lib/scruffy/layers/bar.rb
@@ -0,0 +1,52 @@
+module Scruffy::Layers
+ # ==Scruffy::Layers::Bar
+ #
+ # Author:: Brasten Sager
+ # Date:: August 6th, 2006
+ #
+ # Standard bar graph.
+ class Bar < Base
+
+ # Draw bar graph.
+ def draw(svg, coords, options = {})
+ coords.each do |coord|
+ x, y, bar_height = (coord.first-(@bar_width * 0.5)), coord.last, (height - coord.last)
+
+ svg.g(:transform => "translate(-#{relative(0.5)}, -#{relative(0.5)})") {
+ svg.rect( :x => x, :y => y, :width => @bar_width + relative(1), :height => bar_height + relative(1),
+ :style => "fill: black; fill-opacity: 0.15; stroke: none;" )
+ svg.rect( :x => x+relative(0.5), :y => y+relative(2), :width => @bar_width + relative(1), :height => bar_height - relative(0.5),
+ :style => "fill: black; fill-opacity: 0.15; stroke: none;" )
+
+ }
+
+ svg.rect( :x => x, :y => y, :width => @bar_width, :height => bar_height,
+ :fill => color.to_s, 'style' => "opacity: #{opacity}; stroke: none;" )
+ end
+ end
+
+ protected
+
+ # Due to the size of the bar graph, X-axis coords must
+ # be squeezed so that the bars do not hang off the ends
+ # of the graph.
+ #
+ # Unfortunately this just mean that bar-graphs and most other graphs
+ # end up on different points. Maybe adding a padding to the coordinates
+ # should be a graph-wide thing?
+ def generate_coordinates(options = {})
+ @bar_width = (width / points.size) * 0.9
+ options[:point_distance] = (width - (width / points.size)) / (points.size - 1).to_f
+
+ #TODO more array work with index, try to rework to be accepting of hashes
+ coords = (0...points.size).map do |idx|
+ x_coord = (options[:point_distance] * idx) + (width / points.size * 0.5)
+
+ relative_percent = ((points[idx] == min_value) ? 0 : ((points[idx] - min_value) / (max_value - min_value).to_f))
+ y_coord = (height - (height * relative_percent))
+ [x_coord, y_coord]
+ end
+ coords
+ end
+ end
+end
180 lib/scruffy/layers/base.rb
@@ -0,0 +1,180 @@
+module Scruffy::Layers
+ # ==Scruffy::Layers::Base
+ #
+ # Author:: Brasten Sager
+ # Extended By:: A.J. Ostman
+ # Created:: August 5th, 2006
+ # Last Modified:: August 27, 2006
+ #
+ # Scruffy::Layers::Base contains the basic functionality needed by the various types of graphs. The Base
+ # class is responsible holding layer information such as the title and data points.
+ #
+ # When the graph is rendered, the graph renderer calls Base#render. Base#render sets up
+ # some standard information, and calculates the x,y coordinates of each data point. The draw() method,
+ # which should have been overridden by the current instance, is then called. The actual rendering of
+ # the graph takes place there.
+ #
+ # ====Create New Graph Types
+ #
+ # Assuming the information generated by Scruffy::Layers::Base is sufficient, you can create a new graph type
+ # simply by overriding the draw() method. See Base#draw for arguments.
+ #
+ class Base
+ # The following attributes are user-definable at any time.
+ # title, points, relevant_data, preferred_color, options
+ attr_accessor :title
+ attr_accessor :points
+ attr_accessor :relevant_data
+ attr_accessor :preferred_color
+ attr_accessor :options # On-the-fly values for easy customization / acts as attributes.
+
+ # The following attributes are set during the layer's render process,
+ # and act more as a record of what just happened for later processes.
+ # height, width, min_value, max_value, color, opacity, complexity
+ attr_reader :height, :width
+ attr_reader :min_value, :max_value
+ attr_reader :color
+ attr_reader :opacity
+ attr_reader :complexity
+
+ # Returns a new Base object.
+ #
+ # Any options other that those specified below are stored in the @options variable for
+ # possible later use. This would be a good place to store options needed for a custom
+ # graph.
+ #
+ # Options:
+ # title:: Name/title of data group
+ # points:: Array of data points
+ # preferred_color:: Color used to render this graph, overrides theme color.
+ # relevant_data:: Rarely used - indicates the data on this graph should not
+ # included in any graph data aggregations, such as averaging data points.
+ def initialize(options = {})
+ @title = options.delete(:title) || ''
+ @preferred_color = options.delete(:preferred_color)
+ @relevant_data = options.delete(:relevant_data) || true
+ @points = options.delete(:points) || []
+ @points.extend Scruffy::Helpers::PointContainer unless @points.kind_of? Scruffy::Helpers::PointContainer
+
+ @options = options
+ end
+
+ # Builds SVG code for this graph using the provided Builder object.
+ # This method actually generates data needed by this graph, then passes the
+ # rendering responsibilities to Base#draw.
+ #
+ # svg:: a Builder object used to create SVG code.
+ def render(svg, options = {})
+ setup_variables(options)
+ coords = generate_coordinates(options)
+
+ draw(svg, coords, options)
+ end
+
+ # The method called by Base#draw to render the graph.
+ #
+ # svg:: a Builder object to use for creating SVG code.
+ # coords:: An array of coordinates relating to the graph's data points. ie: [[100, 120], [200, 140], [300, 40]]
+ # options:: Optional arguments.
+ def draw(svg, coords, options={})
+ raise RenderError, "You must override the Base#draw method."
+ end
+
+ # Returns a hash with information to be used by the legend.
+ #
+ # Alternatively, returns nil if you don't want this layer to be in the legend,
+ # or an array of hashes if this layer should have multiple legend entries (stacked?)
+ #
+ # By default, #legend_data returns nil automatically if relevant_data is set to false
+ # or the @color attribute is nil. @color is set when the layer is rendered, so legends
+ # must be rendered AFTER layers.
+ def legend_data
+ if relevant_data? && @color
+ {:title => title,
+ :color => @color,
+ :priority => :normal}
+ else
+ nil
+ end
+ end
+
+ # Returns the value of relevant_data
+ def relevant_data?
+ @relevant_data
+ end
+
+ # The highest data point on this layer, or nil if relevant_data == false
+ def top_value
+ @relevant_data ? points.maximum_value : nil
+ end
+
+ # The lowest data point on this layer, or nil if relevant_data == false
+ def bottom_value
+ @relevant_data ? points.minimum_value : nil
+ end
+
+ # The sum of all values
+ def sum_values
+ points.sum
+ end
+
+ protected
+ # Sets up several variables that almost every graph layer will need to render
+ # itself.
+ def setup_variables(options = {})
+ @color = (preferred_color || options.delete(:color))
+ @width, @height = options.delete(:size)
+ @min_value, @max_value = options[:min_value], options[:max_value]
+ @opacity = options[:opacity] || 1.0
+ @complexity = options[:complexity]
+ end
+
+ # Optimistic generation of coordinates for layer to use. These coordinates are
+ # just a best guess, and can be overridden or thrown away (for example, this is overridden
+ # in pie charting and bar charts).
+ def generate_coordinates(options = {})
+ options[:point_distance] = width / (points.size - 1).to_f
+
+ points.inject_with_index([]) do |memo, point, idx|
+ x_coord = options[:point_distance] * idx
+
+ if point
+ relative_percent = ((point == min_value) ? 0 : ((point - min_value) / (max_value - min_value).to_f))
+ y_coord = (height - (height * relative_percent))
+
+ memo << [x_coord, y_coord]
+ end
+
+ memo
+ end
+ end
+
+ # Converts a percentage into a pixel value, relative to the height.
+ #
+ # Example:
+ # relative(5) # On a 100px high layer, this returns 5. 200px high layer, this returns 10, etc.
+ def relative(pct)
+ # Default to Relative Height
+ relative_height(pct)
+ end
+
+ def relative_width(pct)
+ if pct # Added to handle nils
+ @width * (pct / 100.to_f)
+ end
+ end
+
+ def relative_height(pct)
+ if pct # Added to handle nils
+ @height * (pct / 100.to_f)
+ end
+ end
+
+ # Some SVG elements take a long string of multiple coordinates. This is here
+ # to make that a little easier.
+ def stringify_coords(coords) # :nodoc:
+ coords.map { |c| c.join(',') }
+ end
+ end
+
+end # scruffy::layers
29 lib/scruffy/layers/line.rb
@@ -0,0 +1,29 @@
+module Scruffy::Layers
+ # ==Scruffy::Layers::Line
+ #
+ # Author:: Brasten Sager
+ # Date:: August 7th, 2006
+ #
+ # Line graph.
+ class Line < Base
+
+ # Renders line graph.
+ def draw(svg, coords, options={})
+ svg.g(:class => 'shadow', :transform => "translate(#{relative(0.5)}, #{relative(0.5)})") {
+ svg.polyline( :points => stringify_coords(coords).join(' '), :fill => 'transparent',
+ :stroke => 'black', 'stroke-width' => relative(2),
+ :style => 'fill-opacity: 0; stroke-opacity: 0.35' )
+
+ coords.each { |coord| svg.circle( :cx => coord.first, :cy => coord.last + relative(0.9), :r => relative(2),
+ :style => "stroke-width: #{relative(2)}; stroke: black; opacity: 0.35;" ) }
+ }
+
+
+ svg.polyline( :points => stringify_coords(coords).join(' '), :fill => 'none',
+ :stroke => color.to_s, 'stroke-width' => relative(2) )
+
+ coords.each { |coord| svg.circle( :cx => coord.first, :cy => coord.last, :r => re