From: Date: Fri, 17 Aug 2007 22:07:59 +0000 (-0400) Subject: Added Sparklines controller and dependency, see README. Created method and table... X-Git-Url: https://projects.mako.cc/source/selectricity/commitdiff_plain/a16962155a3c3c6616bfe32c7216f3631836d38c Added Sparklines controller and dependency, see README. Created method and table for margins of victory (i.e won by how much) and ties, in rubyvote, has already been svn'd. --- a16962155a3c3c6616bfe32c7216f3631836d38c diff --cc README index 88fcd97,88fcd97..22f3eb7 --- a/README +++ b/README @@@ -6,7 -6,7 +6,8 @@@ To use Selectricity, you'll need to ins addition to rails and its dependencies: * rmagick -- * gruff ++ * gruff (http://nubyonrails.com/pages/gruff) ++ * sparklines (http://nubyonrails.com/pages/sparklines) To use Selectricity in development mode, you'll need to install the following gems: diff --cc app/controllers/graph_controller.rb index 8b617da,8b617da..1bde09e --- a/app/controllers/graph_controller.rb +++ b/app/controllers/graph_controller.rb @@@ -1,6 -1,6 +1,5 @@@ require 'date' --class GraphController < ApplicationController -- ++class GraphController < ApplicationController class GruffGraff def initialize(options) @@@ -79,9 -79,9 +78,6 @@@ def borda_bar @election = Election.find(params[:id]) -- #pref_tally = make_preference_tally(@election) -- -- #@borda_result = BordaVote.new(pref_tally).result @election.results unless @election.borda_result data, labels = get_borda_points(@election.borda_result) @@@ -97,7 -97,7 +93,8 @@@ #Acording to Tufte, small, concomparitive, highly labeled data sets usually # belong in tables. The following is a bar graph...but would it be better #as a table? -- def choices_positions ++ def choices_positions ++ @election = Election.find(params[:id]) pref_tally = make_preference_tally(@election) @@@ -118,37 -118,37 +115,40 @@@ end private -- def get_positions_info(election) buckets = Hash.new buckets2= Hash.new rank_labels = Hash.new -- ++ election.candidates.each do |candidate| buckets[candidate.id] = [] buckets2[candidate.id] = [] end -- ++ ++ #attach the ranking to the candidate's array to which is belongs election.votes.each do |vote| vote.rankings.each do |ranking| buckets[ranking.candidate_id] << ranking.rank end end -- ++ ++ #count how many times each candidate has been ranked at a certain level buckets.each_pair do |id, array| (1..election.candidates.size).each do |i| buckets2[id] << (array.find_all {|rank| rank == i}).size end end ++ #sort by amount of 1st place votes ++ sorted_data = buckets2.values.sort {|a,b| b[0] <=> a[0]} ++ election.votes.each do |vote| vote.rankings.size.times do |i| rank_labels[i] = (i+1).to_s end end -- return buckets2.values, rank_labels -- ++ return sorted_data, rank_labels end # generate the data and labels for each graph @@@ -256,7 -256,7 +256,7 @@@ #Populate points with an sorted array from election.votes hash #biggest to smallest will go from left to right -- points = result.election.votes.sort do |a, b| ++ points = result.points.sort do |a, b| b[1] <=> a[1] end.collect {|i| i[1]} diff --cc app/controllers/sparklines_controller.rb index 0000000,0000000..eb5e6a6 new file mode 100644 --- /dev/null +++ b/app/controllers/sparklines_controller.rb @@@ -1,0 -1,0 +1,43 @@@ ++class SparklinesController < ApplicationController ++ # Handles requests for sparkline graphs from views. ++ # ++ # Params are generated by the sparkline_tag helper method. ++ # ++ def index ++ # Make array from comma-delimited list of data values ++ ary = [] ++ if params.has_key?('results') && !params['results'].nil? ++ params['results'].split(',').each do |s| ++ ary << s.to_i ++ end ++ end ++ ++ send_data( Sparklines.plot( ary, params ), ++ :disposition => 'inline', ++ :type => 'image/png', ++ :filename => "spark_#{params[:type]}.png" ) ++ end ++ ++ def spark_pie ++ send_data(Sparklines.plot( )) ++ end ++ # Use this type of method for sparklines that can be cached. (Doesn't work with the helper.) ++ # ++ # To make caching easier, add a line like this to config/routes.rb: ++ # map.sparklines "sparklines/:action/:id/image.png", :controller => "sparklines" ++ # ++ # Then reference it with the named route: ++ # image_tag sparklines_url(:action => 'show', :id => 42) ++ def show ++ send_data(Sparklines.plot( ++ [42, 37, 89, 74, 70, 50, 40, 30, 40, 50], ++ :type => 'bar', :above_color => 'orange' ++ ), ++ :disposition => 'inline', ++ :type => 'image/png', ++ :filename => "sparkline.png") ++ end ++ ++ ++ ++end diff --cc app/models/election.rb index 3baeef0,cf7bba5..d76c13f --- a/app/models/election.rb +++ b/app/models/election.rb @@@ -4,6 -4,6 +4,12 @@@ class Election < ActiveRecord::Bas has_many :votes belongs_to :user validates_presence_of :name, :description ++ ++ attr_reader :plurality_result ++ attr_reader :approval_result ++ attr_reader :condorcet_result ++ attr_reader :ssd_result ++ attr_reader :borda_result require 'date' @@@ -76,4 -76,4 +82,45 @@@ longdesc = description.split(/\n/)[1..-1].join("") longdesc.length > 0 ? longdesc : nil end ++ ++ #Calculate Election Results ++ def results ++ # initalize the tallies to empty arrays ++ preference_tally = Array.new ++ plurality_tally = Array.new ++ approval_tally = Array.new ++ ++ self.voters.each do |voter| ++ # skip if the voter has not voted or has an unconfirmed vote ++ next unless voter.voted? ++ ++ plurality_tally << voter.vote.rankings.sort[0].candidate.id ++ approval_tally << voter.vote.rankings.sort[0..1].collect \ ++ { |ranking| ranking.candidate.id } ++ preference_tally << voter.vote.rankings.sort.collect \ ++ { |ranking| ranking.candidate.id } ++ end ++ @plurality_result = PluralityVote.new(plurality_tally).result ++ @approval_result = ApprovalVote.new(approval_tally).result ++ @condorcet_result = PureCondorcetVote.new(preference_tally).result ++ @ssd_result = CloneproofSSDVote.new(preference_tally).result ++ @borda_result = BordaVote.new(preference_tally).result ++ #@runoff_result = InstantRunoffVote.new(preference_tally).result ++ ++ nil # to stay consistent ++ end ++ ++ def names_by_id ++ names = Hash.new ++ ++ competitors = self.candidates.sort.collect {|candidate| candidate.id} ++ competitors.each do |candidate| ++ names[candidate] = Candidate.find(candidate).name ++ end ++ ++ names ++ end ++ end ++ ++ diff --cc app/models/quick_vote.rb index 698dac3,13e6168..c0367d6 --- a/app/models/quick_vote.rb +++ b/app/models/quick_vote.rb @@@ -1,34 -1,19 +1,29 @@@ class QuickVote < Election after_validation :create_candidates validates_uniqueness_of :name + validates_presence_of :name attr_accessor :raw_candidates attr_accessor :reviewed -- attr_accessor :plurality_result -- attr_accessor :approval_result -- attr_accessor :condorcet_result -- attr_accessor :ssd_result -- attr_accessor :borda_result -- ++ def validate if not @raw_candidates or @raw_candidates.length < 2 - errors.add(nil, "You must list at least two candidates.") + errors.add(nil, "You must list at least two candidates.") end + @raw_candidates.each do |c| + unless c.instance_of? String + errors.add(nil, "Candidates must be strings") + next + end + c.strip! + if c.length == 0 + errors.add(nil, "Candidate name must not be empty") + next + end + end if @raw_candidates + + errors.add(nil, "Candidates must all be unique") if @raw_candidates and @raw_candidates.uniq! + if name =~ /[^A-Za-z0-9]/ errors.add(:name, "must only include numbers and letters.") end @@@ -69,33 -54,33 +64,6 @@@ end end -- #Calculate Election Results -- def results -- # initalize the tallies to empty arrays -- preference_tally = Array.new -- plurality_tally = Array.new -- approval_tally = Array.new -- -- self.voters.each do |voter| -- # skip if the voter has not voted or has an unconfirmed vote -- next unless voter.voted? -- -- plurality_tally << voter.vote.rankings.sort[0].candidate.id -- approval_tally << voter.vote.rankings.sort[0..1].collect \ -- { |ranking| ranking.candidate.id } -- preference_tally << voter.vote.rankings.sort.collect \ -- { |ranking| ranking.candidate.id } -- end -- @plurality_result = PluralityVote.new(plurality_tally).result -- @approval_result = ApprovalVote.new(approval_tally).result -- @condorcet_result = PureCondorcetVote.new(preference_tally).result -- @ssd_result = CloneproofSSDVote.new(preference_tally).result -- @borda_result = BordaVote.new(preference_tally).result -- #@runoff_result = InstantRunoffVote.new(preference_tally).result -- #@runoff_results = PluralityVote.new(preference_tally).result -- -- end -- ### Convert a shortname or id into a QuickVote def self.ident_to_quickvote(ident) return nil unless ident diff --cc app/views/quickvote/_defeats_list.rhtml index 248c294,248c294..0000000 deleted file mode 100644,100644 --- a/app/views/quickvote/_defeats_list.rhtml +++ /dev/null @@@ -1,15 -1,15 +1,0 @@@ --<% victories, tied =@election.condorcet_result.list_defeats -%> --<%candidates = @election.candidates.sort.collect {|candidate| candidate.id}%> --<% names = Hash.new -%> --<% candidates.each do |candidate| -%> -- <%names[candidate] = Candidate.find(candidate).name -%> --<% end -%> -- --<% victories.each do |victory| -%> --<%= names[victory[0]] %> beat <%= names[victory[1]] %> by <%= victory[2]%> --votes.
--<% end -%> -- --<% tied.each do |tie| -%> --<%= names[tie[0]]%> tied with <%= names[tie[1]]%>
--<% end -%> diff --cc app/views/quickvote/_pref_table.rhtml index 32f5de4,32f5de4..4576a0f --- a/app/views/quickvote/_pref_table.rhtml +++ b/app/views/quickvote/_pref_table.rhtml @@@ -1,4 -1,4 +1,6 @@@ <% candidates = @election.candidates.sort.collect {|candidate| candidate.id}-%> ++<% voters = @election.voters.size %> ++ <% names = Hash.new -%> <% candidates.each do |candidate| -%> <%names[candidate] = Candidate.find(candidate).name -%> @@@ -16,7 -16,7 +18,11 @@@ <% if winner == loser -%> -- <% else %> -- <%= @election.condorcet_result.matrix[winner][loser] %> ++ <% wins = @election.condorcet_result.matrix[winner][loser]%> ++ <%= wins %> ++ <%= sparkline_tag [(wins.to_f/voters.to_f)*100.0], :type => 'pie', ++ :diameter => 25, :share_color => '#74ce00' %> ++ <% end -%> <% end -%> diff --cc app/views/quickvote/_victories_ties.rhtml index 0000000,0000000..7c3506a new file mode 100644 --- /dev/null +++ b/app/views/quickvote/_victories_ties.rhtml @@@ -1,0 -1,0 +1,17 @@@ ++<% victories, tied = @election.condorcet_result.victories_and_ties %> ++<% names = @election.names_by_id %> ++<% %> ++ ++ <% victories.keys.each do |victor| %> ++ ++ ++ <% victories[victor].keys.each do |loser| %> ++ ++ <% end -%> ++ ++ <% end -%> ++
<%= names[victor] %><%= names[loser] %> (<%= victories[victor][loser] %>)
++ ++ ++ ++ diff --cc app/views/quickvote/results.rhtml index 0cbd6d0,0cbd6d0..c49d682 --- a/app/views/quickvote/results.rhtml +++ b/app/views/quickvote/results.rhtml @@@ -1,3 -1,3 +1,4 @@@ ++<% %> <%require 'whois/whois' %>

Results

@@@ -175,7 -175,7 +176,7 @@@ by several other names.

--<%= render :partial => 'defeats_list' %> ++<%= render :partial => 'victories_ties' %> <%= render :partial => 'pref_table' %> <%= image_tag( graph_url( :action => 'votes_per_day', :id => @election ) ) %>
diff --cc config/environment.rb index ba92734,ba92734..e071a14 --- a/config/environment.rb +++ b/config/environment.rb @@@ -65,6 -65,6 +65,7 @@@ require 'uniq_token require 'randarray' require 'rubyvote' require 'gruff' ++require 'sparklines' class String # alternate capitalization method that does not lowercase the rest of diff --cc lib/rubyvote/condorcet.rb index f86e59d,f86e59d..7fda91b --- a/lib/rubyvote/condorcet.rb +++ b/lib/rubyvote/condorcet.rb @@@ -157,6 -157,6 +157,7 @@@ class CondorcetResult < ElectionResul def victories_and_ties victors = Array.new ties = Array.new ++ victories = Hash.new candidates = @matrix.keys.sort candidates.each do |candidate| @@@ -170,8 -170,8 +171,15 @@@ end end end -- -- victories = victors.sort {|a,b| b[2] <=> a[2]} ++ ++ victors.each do |list| ++ if victories.has_key?(list[0]) ++ victories[list[0]][list[1]] = list[2] ++ else ++ victories[list[0]] = Hash.new ++ victories[list[0]][list[1]] = list[2] ++ end ++ end return victories, ties end diff --cc test/functional/sparklines_controller_test.rb index 0000000,0000000..7d58be4 new file mode 100644 --- /dev/null +++ b/test/functional/sparklines_controller_test.rb @@@ -1,0 -1,0 +1,30 @@@ ++require File.dirname(__FILE__) + '/../test_helper' ++require 'sparklines_controller' ++ ++# Re-raise errors caught by the controller. ++class SparklinesController; def rescue_action(e) raise e end; end ++ ++class SparklinesControllerTest < Test::Unit::TestCase ++ ++ #fixtures :data ++ ++ def setup ++ @controller = SparklinesController.new ++ @request = ActionController::TestRequest.new ++ @response = ActionController::TestResponse.new ++ end ++ ++ def test_index ++ get :index, :results => "1,2,3,4,5", :type => 'bar', :line_color => 'black' ++ assert_response :success ++ assert_equal 'image/png', @response.headers['Content-Type'] ++ 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 --cc vendor/plugins/sparklines/MIT-LICENSE index 0000000,0000000..8d1b480 new file mode 100644 --- /dev/null +++ b/vendor/plugins/sparklines/MIT-LICENSE @@@ -1,0 -1,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 --cc vendor/plugins/sparklines/README index 0000000,0000000..800b508 new file mode 100644 --- /dev/null +++ b/vendor/plugins/sparklines/README @@@ -1,0 -1,0 +1,15 @@@ ++== Sparklines Plugin ++ ++Make tiny graphs. ++ ++Also has a "sparklines" generator for copying a controller and functional test to your app. ++ ++ ./script/generate sparklines ++ ++See examples at http://nubyonrails.com/pages/sparklines ++ ++== Author ++ ++Geoffrey Grosenbach boss@topfunky.com http://nubyonrails.com ++ ++ diff --cc vendor/plugins/sparklines/Rakefile index 0000000,0000000..d3456e4 new file mode 100644 --- /dev/null +++ b/vendor/plugins/sparklines/Rakefile @@@ -1,0 -1,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 --cc vendor/plugins/sparklines/about.yml index 0000000,0000000..f018110 new file mode 100644 --- /dev/null +++ b/vendor/plugins/sparklines/about.yml @@@ -1,0 -1,0 +1,7 @@@ ++author: topfunky ++summary: A library for making small graphs, a la Edward Tufte. A generator and helper are also included. ++homepage: http://nubyonrails.com/pages/sparklines ++plugin: http://topfunky.net/svn/plugins/sparklines ++license: MIT ++version: 0.4.0 ++rails_version: 1.1+ diff --cc vendor/plugins/sparklines/generators/sparklines/sparklines_generator.rb index 0000000,0000000..40e8dae new file mode 100644 --- /dev/null +++ b/vendor/plugins/sparklines/generators/sparklines/sparklines_generator.rb @@@ -1,0 -1,0 +1,13 @@@ ++class SparklinesGenerator < Rails::Generator::Base ++ ++ def manifest ++ record do |m| ++ m.directory File.join("app/controllers") ++ m.directory File.join("test/functional") ++ ++ m.file "controller.rb", File.join("app/controllers/sparklines_controller.rb") ++ m.file "functional_test.rb", File.join("test/functional/sparklines_controller_test.rb") ++ end ++ end ++ ++end diff --cc vendor/plugins/sparklines/generators/sparklines/templates/controller.rb index 0000000,0000000..1c69f29 new file mode 100644 --- /dev/null +++ b/vendor/plugins/sparklines/generators/sparklines/templates/controller.rb @@@ -1,0 -1,0 +1,40 @@@ ++class SparklinesController < ApplicationController ++ ++ # Handles requests for sparkline graphs from views. ++ # ++ # Params are generated by the sparkline_tag helper method. ++ # ++ def index ++ # Make array from comma-delimited list of data values ++ ary = [] ++ if params.has_key?('results') && !params['results'].nil? ++ params['results'].split(',').each do |s| ++ ary << s.to_i ++ end ++ end ++ ++ send_data( Sparklines.plot( ary, params ), ++ :disposition => 'inline', ++ :type => 'image/png', ++ :filename => "spark_#{params[:type]}.png" ) ++ end ++ ++ ++ # Use this type of method for sparklines that can be cached. (Doesn't work with the helper.) ++ # ++ # To make caching easier, add a line like this to config/routes.rb: ++ # map.sparklines "sparklines/:action/:id/image.png", :controller => "sparklines" ++ # ++ # Then reference it with the named route: ++ # image_tag sparklines_url(:action => 'show', :id => 42) ++ def show ++ send_data(Sparklines.plot( ++ [42, 37, 89, 74, 70, 50, 40, 30, 40, 50], ++ :type => 'bar', :above_color => 'orange' ++ ), ++ :disposition => 'inline', ++ :type => 'image/png', ++ :filename => "sparkline.png") ++ end ++ ++end diff --cc vendor/plugins/sparklines/generators/sparklines/templates/functional_test.rb index 0000000,0000000..7d58be4 new file mode 100644 --- /dev/null +++ b/vendor/plugins/sparklines/generators/sparklines/templates/functional_test.rb @@@ -1,0 -1,0 +1,30 @@@ ++require File.dirname(__FILE__) + '/../test_helper' ++require 'sparklines_controller' ++ ++# Re-raise errors caught by the controller. ++class SparklinesController; def rescue_action(e) raise e end; end ++ ++class SparklinesControllerTest < Test::Unit::TestCase ++ ++ #fixtures :data ++ ++ def setup ++ @controller = SparklinesController.new ++ @request = ActionController::TestRequest.new ++ @response = ActionController::TestResponse.new ++ end ++ ++ def test_index ++ get :index, :results => "1,2,3,4,5", :type => 'bar', :line_color => 'black' ++ assert_response :success ++ assert_equal 'image/png', @response.headers['Content-Type'] ++ 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 --cc vendor/plugins/sparklines/init.rb index 0000000,0000000..c9c2349 new file mode 100644 --- /dev/null +++ b/vendor/plugins/sparklines/init.rb @@@ -1,0 -1,0 +1,1 @@@ ++ActionView::Base.send :include, SparklinesHelper diff --cc vendor/plugins/sparklines/lib/sparklines.rb index 0000000,0000000..4c4e2de new file mode 100644 --- /dev/null +++ b/vendor/plugins/sparklines/lib/sparklines.rb @@@ -1,0 -1,0 +1,582 @@@ ++ ++require 'rubygems' ++require 'RMagick' ++ ++=begin rdoc ++ ++A library for generating small unmarked graphs (sparklines). ++ ++Can be used to write an image to a file or make a web service with Rails or other Ruby CGI apps. ++ ++Idea and much of the outline for the source lifted directly from {Joe Gregorio's Python Sparklines web service script}[http://bitworking.org/projects/sparklines]. ++ ++Requires the RMagick image library. ++ ++==Authors ++ ++{Dan Nugent}[mailto:nugend@gmail.com] Original port from Python Sparklines library. ++ ++{Geoffrey Grosenbach}[mailto:boss@topfunky.com] -- http://nubyonrails.topfunky.com ++-- Conversion to module and further maintenance. ++ ++==General Usage and Defaults ++ ++To use in a script: ++ ++ require 'rubygems' ++ require 'sparklines' ++ Sparklines.plot([1,25,33,46,89,90,85,77,42], ++ :type => 'discrete', ++ :height => 20) ++ ++An image blob will be returned which you can print, write to STDOUT, etc. ++ ++For use with Ruby on Rails, see the sparklines plugin: ++ ++ http://nubyonrails.com/pages/sparklines ++ ++In your view, call it like this: ++ ++ <%= sparkline_tag [1,2,3,4,5,6] %> ++ ++Or specify details: ++ ++ <%= sparkline_tag [1,2,3,4,5,6], ++ :type => 'discrete', ++ :height => 10, ++ :upper => 80, ++ :above_color => 'green', ++ :below_color => 'blue' %> ++ ++Graph types: ++ ++ area ++ discrete ++ pie ++ smooth ++ bar ++ whisker ++ ++General Defaults: ++ ++ :type => 'smooth' ++ :height => 14px ++ :upper => 50 ++ :above_color => 'red' ++ :below_color => 'grey' ++ :background_color => 'white' ++ :line_color => 'lightgrey' ++ ++==License ++ ++Licensed under the MIT license. ++ ++=end ++class Sparklines ++ ++ VERSION = '0.4.1' ++ ++ @@label_margin = 5.0 ++ @@pointsize = 10.0 ++ ++ class << self ++ ++ # Does the actual plotting of the graph. ++ # Calls the appropriate subclass based on the :type argument. ++ # Defaults to 'smooth' ++ def plot(data=[], options={}) ++ defaults = { ++ :type => 'smooth', ++ :height => 14, ++ :upper => 50, ++ :diameter => 20, ++ :step => 2, ++ :line_color => 'lightgrey', ++ ++ :above_color => 'red', ++ :below_color => 'grey', ++ :background_color => 'white', ++ :share_color => 'red', ++ :remain_color => 'lightgrey', ++ :min_color => 'blue', ++ :max_color => 'green', ++ :last_color => 'red', ++ ++ :has_min => false, ++ :has_max => false, ++ :has_last => false, ++ ++ :label => nil ++ } ++ ++ # HACK for HashWithIndifferentAccess ++ options_sym = Hash.new ++ options.keys.each do |key| ++ options_sym[key.to_sym] = options[key] ++ end ++ ++ options_sym = defaults.merge(options_sym) ++ ++ # Call the appropriate method for actual plotting. ++ sparkline = self.new(data, options_sym) ++ if %w(area bar pie smooth discrete whisker).include? options_sym[:type] ++ sparkline.send options_sym[:type] ++ else ++ sparkline.plot_error options_sym ++ end ++ end ++ ++ # Writes a graph to disk with the specified filename, or "sparklines.png" ++ def plot_to_file(filename="sparklines.png", data=[], options={}) ++ File.open( filename, 'wb' ) do |png| ++ png << self.plot( data, options) ++ end ++ end ++ ++ end # class methods ++ ++ def initialize(data=[], options={}) ++ @data = Array(data) ++ @options = options ++ normalize_data ++ end ++ ++ ## ++ # Creates a continuous area sparkline. Relevant options. ++ # ++ # :step - An integer that determines the distance between each point on the sparkline. Defaults to 2. ++ # ++ # :height - An integer that determines what the height of the sparkline will be. Defaults to 14 ++ # ++ # :upper - An integer that determines the threshold for colorization purposes. Any value less than upper will be colored using below_color, anything above and equal to upper will use above_color. Defaults to 50. ++ # ++ # :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaults to false. ++ # ++ # :has_max - Determines whether a dot will be drawn at the highest value or not. Defaults to false. ++ # ++ # :has_last - Determines whether a dot will be drawn at the last value or not. Defaults to false. ++ # ++ # :min_color - A string or color code representing the color that the dot drawn at the smallest value will be displayed as. Defaults to blue. ++ # ++ # :max_color - A string or color code representing the color that the dot drawn at the largest value will be displayed as. Defaults to green. ++ # ++ # :last_color - A string or color code representing the color that the dot drawn at the last value will be displayed as. Defaults to red. ++ # ++ # :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red. ++ # ++ # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray. ++ ++ def area ++ ++ step = @options[:step].to_i ++ height = @options[:height].to_i ++ background_color = @options[:background_color] ++ ++ create_canvas((@norm_data.size - 1) * step + 4, height, background_color) ++ ++ upper = @options[:upper].to_i ++ ++ has_min = @options[:has_min] ++ has_max = @options[:has_max] ++ has_last = @options[:has_last] ++ ++ min_color = @options[:min_color] ++ max_color = @options[:max_color] ++ last_color = @options[:last_color] ++ below_color = @options[:below_color] ++ above_color = @options[:above_color] ++ ++ ++ coords = [[0,(height - 3 - upper/(101.0/(height-4)))]] ++ i=0 ++ @norm_data.each do |r| ++ coords.push [(2 + i), (height - 3 - r/(101.0/(height-4)))] ++ i += step ++ end ++ coords.push [(@norm_data.size - 1) * step + 4, (height - 3 - upper/(101.0/(height-4)))] ++ ++ # TODO Refactor! Should take a block and do both. ++ # ++ # Block off the bottom half of the image and draw the sparkline ++ @draw.fill(above_color) ++ @draw.define_clip_path('top') do ++ @draw.rectangle(0,0,(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4)))) ++ end ++ @draw.clip_path('top') ++ @draw.polygon *coords.flatten ++ ++ # Block off the top half of the image and draw the sparkline ++ @draw.fill(below_color) ++ @draw.define_clip_path('bottom') do ++ @draw.rectangle(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,height) ++ end ++ @draw.clip_path('bottom') ++ @draw.polygon *coords.flatten ++ ++ # The sparkline looks kinda nasty if either the above_color or below_color gets the center line ++ @draw.fill('black') ++ @draw.line(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4)))) ++ ++ # After the parts have been masked, we need to let the whole canvas be drawable again ++ # so a max dot can be displayed ++ @draw.define_clip_path('all') do ++ @draw.rectangle(0,0,@canvas.columns,@canvas.rows) ++ end ++ @draw.clip_path('all') ++ ++ drawbox(coords[@norm_data.index(@norm_data.min)+1], 1, min_color) if has_min == true ++ drawbox(coords[@norm_data.index(@norm_data.max)+1], 1, max_color) if has_max == true ++ ++ drawbox(coords[-2], 1, last_color) if has_last == true ++ ++ @draw.draw(@canvas) ++ @canvas.to_blob ++ end ++ ++ ## ++ # A bar graph. ++ ++ def bar ++ step = @options[:step].to_i ++ height = @options[:height].to_f ++ background_color = @options[:background_color] ++ ++ create_canvas(@norm_data.length * step + 2, height, background_color) ++ ++ upper = @options[:upper].to_i ++ below_color = @options[:below_color] ++ above_color = @options[:above_color] ++ ++ i = 1 ++ @norm_data.each_with_index do |r, index| ++ color = (r >= upper) ? above_color : below_color ++ @draw.stroke('transparent') ++ @draw.fill(color) ++ @draw.rectangle( i, @canvas.rows, ++ i + step - 2, @canvas.rows - ( (r / @maximum_value) * @canvas.rows) ) ++ i += step ++ end ++ ++ @draw.draw(@canvas) ++ @canvas.to_blob ++ end ++ ++ ++ ## ++ # Creates a discretized sparkline ++ # ++ # :height - An integer that determines what the height of the sparkline will be. Defaults to 14 ++ # ++ # :upper - An integer that determines the threshold for colorization purposes. Any value less than upper will be colored using below_color, anything above and equal to upper will use above_color. Defaults to 50. ++ # ++ # :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red. ++ # ++ # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray. ++ ++ def discrete ++ ++ height = @options[:height].to_i ++ upper = @options[:upper].to_i ++ background_color = @options[:background_color] ++ step = @options[:step].to_i ++ ++ create_canvas(@norm_data.size * step - 1, height, background_color) ++ ++ below_color = @options[:below_color] ++ above_color = @options[:above_color] ++ ++ i = 0 ++ @norm_data.each do |r| ++ color = (r >= upper) ? above_color : below_color ++ @draw.stroke(color) ++ @draw.line(i, (@canvas.rows - r/(101.0/(height-4))-4).to_i, ++ i, (@canvas.rows - r/(101.0/(height-4))).to_i) ++ i += step ++ end ++ ++ @draw.draw(@canvas) ++ @canvas.to_blob ++ end ++ ++ ++ ## ++ # Creates a pie-chart sparkline ++ # ++ # :diameter - An integer that determines what the size of the sparkline will be. Defaults to 20 ++ # ++ # :share_color - A string or color code representing the color to draw the share of the pie represented by percent. Defaults to red. ++ # ++ # :remain_color - A string or color code representing the color to draw the pie not taken by the share color. Defaults to lightgrey. ++ ++ def pie ++ diameter = @options[:diameter].to_i ++ background_color = @options[:background_color] ++ ++ create_canvas(diameter, diameter, background_color) ++ ++ share_color = @options[:share_color] ++ remain_color = @options[:remain_color] ++ percent = @norm_data[0] ++ ++ # Adjust the radius so there's some edge left in the pie ++ r = diameter/2.0 - 2 ++ @draw.fill(remain_color) ++ @draw.ellipse(r + 2, r + 2, r , r , 0, 360) ++ @draw.fill(share_color) ++ ++ # Special exceptions ++ if percent == 0 ++ # For 0% return blank ++ @draw.draw(@canvas) ++ return @canvas.to_blob ++ elsif percent == 100 ++ # For 100% just draw a full circle ++ @draw.ellipse(r + 2, r + 2, r , r , 0, 360) ++ @draw.draw(@canvas) ++ return @canvas.to_blob ++ end ++ ++ # Okay, this part is as confusing as hell, so pay attention: ++ # This line determines the horizontal portion of the point on the circle where the X-Axis ++ # should end. It's caculated by taking the center of the on-image circle and adding that ++ # to the radius multiplied by the formula for determinig the point on a unit circle that a ++ # angle corresponds to. 3.6 * percent gives us that angle, but it's in degrees, so we need to ++ # convert, hence the muliplication by Pi over 180 ++ arc_end_x = r + 2 + (r * Math.cos((3.6 * percent)*(Math::PI/180))) ++ ++ # The same goes for here, except it's the vertical point instead of the horizontal one ++ arc_end_y = r + 2 + (r * Math.sin((3.6 * percent)*(Math::PI/180))) ++ ++ # Because the SVG path format is seriously screwy, we need to set the large-arc-flag to 1 ++ # if the angle of an arc is greater than 180 degrees. I have no idea why this is, but it is. ++ percent > 50? large_arc_flag = 1: large_arc_flag = 0 ++ ++ # This is also confusing ++ # M tells us to move to an absolute point on the image. We're moving to the center of the pie ++ # h tells us to move to a relative point. We're moving to the right edge of the circle. ++ # A tells us to start an absolute elliptical arc. The first two values are the radii of the ellipse ++ # the third value is the x-axis-rotation (how to rotate the ellipse if we wanted to [could have some fun ++ # with randomizing that maybe), the fourth value is our large-arc-flag, the fifth is the sweep-flag, ++ # (again, confusing), the sixth and seventh values are the end point of the arc which we calculated previously ++ # More info on the SVG path string format at: http://www.w3.org/TR/SVG/paths.html ++ path = "M#{r + 2},#{r + 2} h#{r} A#{r},#{r} 0 #{large_arc_flag},1 #{arc_end_x},#{arc_end_y} z" ++ @draw.path(path) ++ ++ @draw.draw(@canvas) ++ @canvas.to_blob ++ end ++ ++ ++ ## ++ # Creates a smooth sparkline. ++ # ++ # :step - An integer that determines the distance between each point on the sparkline. Defaults to 2. ++ # ++ # :height - An integer that determines what the height of the sparkline will be. Defaults to 14 ++ # ++ # :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaults to false. ++ # ++ # :has_max - Determines whether a dot will be drawn at the highest value or not. Defaults to false. ++ # ++ # :has_last - Determines whether a dot will be drawn at the last value or not. Defaults to false. ++ # ++ # :min_color - A string or color code representing the color that the dot drawn at the smallest value will be displayed as. Defaults to blue. ++ # ++ # :max_color - A string or color code representing the color that the dot drawn at the largest value will be displayed as. Defaults to green. ++ # ++ # :last_color - A string or color code representing the color that the dot drawn at the last value will be displayed as. Defaults to red. ++ ++ def smooth ++ ++ step = @options[:step].to_i ++ height = @options[:height].to_i ++ background_color = @options[:background_color] ++ ++ create_canvas((@norm_data.size - 1) * step + 4, height, background_color) ++ ++ min_color = @options[:min_color] ++ max_color = @options[:max_color] ++ last_color = @options[:last_color] ++ has_min = @options[:has_min] ++ has_max = @options[:has_max] ++ has_last = @options[:has_last] ++ line_color = @options[:line_color] ++ ++ @draw.stroke(line_color) ++ coords = [] ++ i=0 ++ @norm_data.each do |r| ++ coords.push [ 2 + i, (height - 3 - r/(101.0/(height-4))) ] ++ i += step ++ end ++ ++ open_ended_polyline(coords) ++ ++ drawbox(coords[@norm_data.index(@norm_data.min)], 2, min_color) if has_min == true ++ ++ drawbox(coords[@norm_data.index(@norm_data.max)], 2, max_color) if has_max == true ++ ++ drawbox(coords[-1], 2, last_color) if has_last == true ++ ++ @draw.draw(@canvas) ++ @canvas.to_blob ++ end ++ ++ ++ ## ++ # Creates a whisker sparkline to track on/off type data. There are five states: ++ # on, off, no value, exceptional on, exceptional off. On values create an up ++ # whisker and off values create a down whisker. Exceptional values may be ++ # colored differently than regular values to indicate, for example, a shut out. ++ # No value produces an empty row to indicate a tie. ++ # ++ # * results - an array of integer values between -2 and 2. -2 is exceptional ++ # down, 1 is regular down, 0 is no value, 1 is up, and 2 is exceptional up. ++ # * options - a hash that takes parameters ++ # ++ # :height - height of the sparkline ++ # ++ # :whisker_color - the color of regular whiskers; defaults to black ++ # ++ # :exception_color - the color of exceptional whiskers; defaults to red ++ ++ def whisker ++ ++ # step = @options[:step].to_i ++ height = @options[:height].to_i ++ background_color = @options[:background_color] ++ ++ create_canvas((@data.size - 1) * 2, height, background_color) ++ ++ whisker_color = @options[:whisker_color] || 'black' ++ exception_color = @options[:exception_color] || 'red' ++ ++ i = 0 ++ @data.each do |r| ++ color = whisker_color ++ ++ if ( (r == 2 || r == -2) && exception_color ) ++ color = exception_color ++ end ++ ++ y_mid_point = (r >= 1) ? (@canvas.rows/2.0 - 1).ceil : (@canvas.rows/2.0).floor ++ ++ y_end_point = y_mid_point ++ if ( r > 0) ++ y_end_point = 0 ++ end ++ ++ if ( r < 0 ) ++ y_end_point = @canvas.rows ++ end ++ ++ @draw.stroke( color ) ++ @draw.line( i, y_mid_point, i, y_end_point ) ++ i += 2 ++ end ++ ++ @draw.draw(@canvas) ++ @canvas.to_blob ++ end ++ ++ ## ++ # Draw the error Sparkline. ++ ++ def plot_error(options={}) ++ create_canvas(40, 15, 'white') ++ ++ @draw.fill('red') ++ @draw.line(0,0,40,15) ++ @draw.line(0,15,40,0) ++ ++ @draw.draw(@canvas) ++ @canvas.to_blob ++ end ++ ++private ++ ++ def normalize_data ++ @maximum_value = @data.max ++ if @options[:type].to_s == 'pie' ++ @norm_data = @data ++ else ++ @norm_data = @data.map { |value| value = (value.to_f / @maximum_value) * 100.0 } ++ end ++ end ++ ++ ## ++ # * :arr - an array of points (represented as two element arrays) ++ ++ def open_ended_polyline(arr) ++ 0.upto(arr.length - 2) { |i| ++ @draw.line(arr[i][0], arr[i][1], arr[i+1][0], arr[i+1][1]) ++ } ++ end ++ ++ ## ++ # Create an image to draw on and a drawable to do the drawing with. ++ # ++ # TODO Refactor into smaller methods ++ ++ def create_canvas(w, h, bkg_col) ++ @draw = Magick::Draw.new ++ @draw.pointsize = @@pointsize # TODO Use height ++ @canvas = Magick::Image.new(w , h) { self.background_color = bkg_col } ++ ++ # Make room for label and last value ++ unless @options[:label].nil? ++ @options[:has_last] = true ++ @label_width = calculate_width(@options[:label]) ++ @data_last_width = calculate_width(@data.last) ++ # HACK The 7.0 is a severe hack. Must figure out correct spacing ++ @label_and_data_last_width = @label_width + @data_last_width + @@label_margin * 7.0 ++ w += @label_and_data_last_width ++ end ++ ++ @canvas = Magick::Image.new(w , h) { self.background_color = bkg_col } ++ @canvas.format = "PNG" ++ ++ # Draw label and last value ++ unless @options[:label].nil? ++ if ENV.has_key?('MAGICK_FONT_PATH') ++ vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH']) ++ @font = File.exists?(vera_font_path) ? vera_font_path : nil ++ else ++ @font = nil ++ end ++ ++ @draw.fill = 'black' ++ @draw.font = @font if @font ++ @draw.gravity = Magick::WestGravity ++ @draw.annotate( @canvas, ++ @label_width, 1.0, ++ w - @label_and_data_last_width + @@label_margin, h - calculate_caps_height/2.0, ++ @options[:label]) ++ ++ @draw.fill = 'red' ++ @draw.annotate( @canvas, ++ @data_last_width, 1.0, ++ w - @data_last_width - @@label_margin * 2.0, h - calculate_caps_height/2.0, ++ @data.last.to_s) ++ end ++ end ++ ++ ## ++ # Utility to draw a coloured box ++ # Centred on pt, offset off in each direction, fill color is col ++ ++ def drawbox(pt, offset, color) ++ @draw.stroke 'transparent' ++ @draw.fill(color) ++ @draw.rectangle(pt[0]-offset, pt[1]-offset, pt[0]+offset, pt[1]+offset) ++ end ++ ++ def calculate_width(text) ++ @draw.get_type_metrics(@canvas, text.to_s).width ++ end ++ ++ def calculate_caps_height ++ @draw.get_type_metrics(@canvas, 'X').height ++ end ++ ++end diff --cc vendor/plugins/sparklines/lib/sparklines_helper.rb index 0000000,0000000..c8958d3 new file mode 100644 --- /dev/null +++ b/vendor/plugins/sparklines/lib/sparklines_helper.rb @@@ -1,0 -1,0 +1,18 @@@ ++# Provides a tag for embedding sparklines graphs into your Rails app. ++# ++module SparklinesHelper ++ ++ # Call with an array of data and a hash of params for the Sparklines module. ++ # ++ # sparkline_tag [42, 37, 43, 182], :type => 'bar', :line_color => 'black' ++ # ++ # You can also pass :class => 'some_css_class' ('sparkline' by default). ++ def sparkline_tag(results=[], options={}) ++ url = { :controller => 'sparklines', ++ :results => results.join(',') } ++ options = url.merge(options) ++ ++ %(Sparkline Graph) ++ end ++ ++end