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