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-live/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| %>
++
++ <%= names[victor] %> |
++ <% victories[victor].keys.each do |loser| %>
++ <%= names[loser] %> (<%= victories[victor][loser] %>) |
++ <% end -%>
++
++ <% end -%>
++
++
++
++
++
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)
++
++ %()
++ end
++
++end