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:
member :voter_id, :int
member :voter_ipaddress, :string
member :vote_time, :int
+ member :vote, [:int]
end
class VoteResultStruct < ActionWebService::Struct
class ApplicationController < ActionController::Base
include AuthenticatedSystem
helper :user
- model :user
+ require_dependency "user"
end
require 'date'
-class GraphController < ApplicationController
-
+class GraphController < ApplicationController
class GruffGraff
def initialize(options)
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)
#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)
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
#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]}
# look to see that the voter has been created and has voted in
# this election, and has confirmed their vote
- @voter = QuickVoter.find_all(["session_id = ? and election_id = ?",
+ @voter = QuickVoter.find(:all, :conditions => ["session_id = ? and election_id = ?",
session.session_id, @election.id])[0]
# if the voter has not voted we destroy them
election = QuickVote.ident_to_quickvote(params[:ident])
# find out who the voter is for this election
- @voter = QuickVoter.find_all(["session_id = ? and election_id = ?",
+ @voter = QuickVoter.find(:all, :conditions => ["session_id = ? and election_id = ?",
session.session_id, election.id])[0]
if not @voter
end
def change
- voter = QuickVoter.find_all(["session_id = ?", session.session_id])[0]
+ voter = QuickVoter.find(:all, :conditions => ["session_id = ?", session.session_id])[0]
voter.destroy
redirect_to quickvote_url( :ident => params[:ident] )
end
--- /dev/null
+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
\ No newline at end of file
def index
password = params[:id]
password = params[:vote][:password] if params[:vote]
- if @voter = FullVoter.find_all( [ "password = ?", password ] )[0]
+ if @voter = FullVoter.find(:all, :conditions => [ "password = ?", password ] )[0]
render :action => 'fullvote'
end
end
private
def authenticate
password = params[:id]
- @voter = FullVoter.find_all( [ "password = ?", password ] )[0]
+ @voter = FullVoter.find(:all, :conditions => [ "password = ?", password ] )[0]
end
end
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'
end
def quickvote?
- type == 'QuickVote'
+ self.class == 'QuickVote'
end
def active?
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
+
+
def create_password
token_generator = UniqueTokenGenerator.new( 16 )
until password and not password.empty? \
- and Voter.find_all( [ "password = ?", password ]).empty?
+ and Voter.find(:all, :conditions => [ "password = ?", password ]).empty?
self.password = token_generator.token
end
end
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
end
def name
- read_attribute( :name ).downcase()
+ read_attribute( :name ).downcase() if read_attribute( :name )
end
def reviewed?
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
if ident.match(/^\d+$/)
quickvote = QuickVote.find(ident)
else
- quickvote = QuickVote.find_all(["name = ?", ident])[0]
+ quickvote = QuickVote.find(:all, :conditions => ["name = ?", ident])[0]
end
return quickvote
if election
candidates=election.candidates.collect { |c| c.id }
vote_list[0].each do |vote|
- raise ArgumentError.new "Invalid Candidate ID #{vote}" unless candidates.index(vote)
+ raise ArgumentError.new("Invalid Candidate ID #{vote}") unless candidates.index(vote)
end
- raise ArgumentError.new "You must rank all candidates" unless candidates.length <= vote_list[0].length
- raise ArgumentError.new "Please rank each candidate only once" if vote_list[0].uniq!
+ raise ArgumentError.new("You must rank all candidates") unless candidates.length <= vote_list[0].length
+ raise ArgumentError.new("Please rank each candidate only once") if vote_list[0].uniq!
voter = QuickVoter.new
voter.election = election
voter.ipaddress = "XMLRPC Request"
voter.vote.confirm!
voter.save!
else
- raise ArgumentError.new "Cannot find election #{election_name}"
+ raise ArgumentError.new("Cannot find election #{election_name}")
end
end
def quickvote_candidate_ids_to_names(shortname, id_list)
return result
end
qv.votes.each do |vote|
- votes << VoteInfo.new(:voter_id => vote.voter.id, :voter_ipaddress => vote.voter.ipaddress, :vote_time => vote.time.to_i)
+ votes << VoteInfo.new(:voter_id => vote.voter.id, :voter_ipaddress => vote.voter.ipaddress, :vote_time => vote.time.to_i, :vote => vote.votes)
end
return votes
end
def list_quickvotes()
all=Array.new
- QuickVote.find_all.each do |election|
+ QuickVote.find(:all).each do |election|
all << get_quickvote(election.name)
end
return all
end
def get_quickvote(shortname)
return ElectionStruct.new unless election=QuickVote.ident_to_quickvote(shortname)
- return ElectionStruct.new (:id => election.id, :name => election.name, :description => election.description, :candidate_ids => election.candidates.collect {|c| c.id }, :candidate_names => election.candidates.collect {|c| c.name } )
+ return ElectionStruct.new(:id => election.id, :name => election.name, :description => election.description, :candidate_ids => election.candidates.collect {|c| c.id }, :candidate_names => election.candidates.collect {|c| c.name } )
end
def create_quickvote(election)
qv=QuickVote.new(:name => election.name, :description => election.description)
qv.candidatelist=election.candidate_names
- return qv.save.to_s
+ if qv.save
+ return ""
+ else
+ return "Saving quickvote FAILED:"+qv.errors.inspect
+ end
end
end
super
token_generator = UniqueTokenGenerator.new( 16 )
- until not token.empty? and Token.find_all( [ "token = ?", token ]).empty?
+ until not token.empty? and Token.find(:all, :conditions => [ "token = ?", token ]).empty?
self.token = token_generator.token
end
+++ /dev/null
-<% 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.<br />
-<% end -%>
-
-<% tied.each do |tie| -%>
-<%= names[tie[0]]%> tied with <%= names[tie[1]]%><br />
-<% end -%>
<% 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 -%>
<% if winner == loser -%>
<td> -- </td>
<% else %>
- <td><%= @election.condorcet_result.matrix[winner][loser] %></td>
+ <td><% 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' %>
+ </td>
<% end -%>
<% end -%>
</tr>
--- /dev/null
+<% victories, tied = @election.condorcet_result.victories_and_ties %>
+<% names = @election.names_by_id %>
+<% %>
+<table class="voterbox">
+ <% victories.keys.each do |victor| %>
+ <tr>
+ <th><%= names[victor] %></th>
+ <% victories[victor].keys.each do |loser| %>
+ <td><%= names[loser] %> (<%= victories[victor][loser] %>)</td>
+ <% end -%>
+ </tr>
+ <% end -%>
+</table>
+
+
+
+
+<% %>
<%require 'whois/whois' %>
<h1>Results</h1>
<% end %>
</table>
-<%= render :partial => 'defeats_list' %>
+<%= render :partial => 'victories_ties' %>
<%= render :partial => 'pref_table' %>
<%= image_tag( graph_url( :action => 'votes_per_day', :id => @election ) ) %><br />
require 'randarray'
require 'rubyvote'
require 'gruff'
+require 'sparklines'
class String
# alternate capitalization method that does not lowercase the rest of
# migrations feature of ActiveRecord to incrementally modify your database, and
# then regenerate this schema definition.
-ActiveRecord::Schema.define(:version => 0) do
+ActiveRecord::Schema.define() do
create_table "candidates", :force => true do |t|
- t.column "election_id", :integer, :default => 0, :null => false
- t.column "name", :string, :limit => 100, :default => "", :null => false
- t.column "picture", :binary, :default => "", :null => false
+ t.column "election_id", :integer, :null => false
+ t.column "name", :string, :limit => 100, :default => "", :null => false
+ t.column "description", :text
+ t.column "picture_filename", :string, :limit => 200
+ t.column "picture_data", :binary
+ t.column "picture_type", :string, :limit => 100
end
create_table "elections", :force => true do |t|
- t.column "name", :string, :limit => 100, :default => "", :null => false
- t.column "description", :text, :default => "", :null => false
- t.column "anonymous", :integer, :limit => 4, :default => 0, :null => false
- t.column "startdate", :datetime, :null => false
- t.column "enddate", :datetime
+ t.column "name", :string, :limit => 100, :default => "", :null => false
+ t.column "description", :text, :default => "", :null => false
+ t.column "anonymous", :integer, :limit => 4, :default => 1, :null => false
+ t.column "startdate", :datetime
+ t.column "enddate", :datetime, :null => false
+ t.column "active", :integer, :limit => 4, :default => 0, :null => false
+ t.column "user_id", :integer
+ t.column "type", :string, :limit => 100, :default => "", :null => false
end
+ add_index "elections", ["user_id"], :name => "fk_user_election"
+
create_table "rankings", :force => true do |t|
- t.column "vote_id", :integer
+ t.column "vote_id", :integer
t.column "candidate_id", :integer
- t.column "rank", :integer
+ t.column "rank", :integer
end
create_table "tokens", :force => true do |t|
- t.column "token", :string, :limit => 100, :default => "", :null => false
- t.column "vote_id", :integer, :default => 0, :null => false
+ t.column "token", :string, :limit => 100, :default => "", :null => false
+ t.column "vote_id", :integer, :null => false
end
add_index "tokens", ["vote_id"], :name => "fk_vote_token"
create_table "users", :force => true do |t|
- t.column "login", :string, :limit => 80, :default => "", :null => false
- t.column "salted_password", :string, :limit => 40, :default => "", :null => false
- t.column "email", :string, :limit => 60, :default => "", :null => false
- t.column "firstname", :string, :limit => 40
- t.column "lastname", :string, :limit => 40
- t.column "salt", :string, :limit => 40, :default => "", :null => false
- t.column "verified", :integer, :default => 0
- t.column "role", :string, :limit => 40
- t.column "security_token", :string, :limit => 40
- t.column "token_expiry", :datetime
- t.column "created_at", :datetime
- t.column "updated_at", :datetime
- t.column "logged_in_at", :datetime
- t.column "deleted", :integer, :default => 0
- t.column "delete_after", :datetime
+ t.column "login", :text
+ t.column "ip", :text, :default => "", :null => false
+ t.column "email", :text
+ t.column "crypted_password", :string, :limit => 40
+ t.column "salt", :string, :limit => 40
+ t.column "created_at", :datetime
+ t.column "updated_at", :datetime
+ t.column "remember_token", :text
+ t.column "remember_token_expires_at", :datetime
end
create_table "voters", :force => true do |t|
- t.column "email", :string, :limit => 100, :default => "", :null => false
- t.column "password", :string, :limit => 100, :default => "", :null => false
- t.column "contacted", :integer, :limit => 4, :default => 0, :null => false
- t.column "election_id", :integer, :default => 0, :null => false
+ t.column "email", :string, :limit => 100
+ t.column "password", :string, :limit => 100
+ t.column "contacted", :integer, :limit => 4, :default => 0, :null => false
+ t.column "election_id", :integer, :null => false
+ t.column "session_id", :string, :limit => 32
+ t.column "ipaddress", :string, :limit => 32
end
add_index "voters", ["election_id"], :name => "fk_election_voter"
create_table "votes", :force => true do |t|
- t.column "voter_id", :integer
- t.column "confirmed", :integer, :limit => 4, :default => 0, :null => false
+ t.column "voter_id", :integer
+ t.column "confirmed", :integer, :limit => 4, :default => 0, :null => false
+ t.column "time", :datetime
end
add_index "votes", ["voter_id"], :name => "fk_vote_voter"
def victories_and_ties
victors = Array.new
ties = Array.new
+ victories = Hash.new
candidates = @matrix.keys.sort
candidates.each do |candidate|
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
+++ /dev/null
-# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
-first:
- id: 1
-another:
- id: 2
+++ /dev/null
-# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
-first:
- id: 1
-another:
- id: 2
+++ /dev/null
-# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
-first:
- id: 1
-another:
- id: 2
require File.dirname(__FILE__) + '/../test_helper'
-require 'elections_controller'
+require 'election_controller'
# Re-raise errors caught by the controller.
-class ElectionsController; def rescue_action(e) raise e end; end
+class ElectionController; def rescue_action(e) raise e end; end
-class ElectionsControllerTest < Test::Unit::TestCase
+class ElectionControllerTest < Test::Unit::TestCase
fixtures :elections
def setup
- @controller = ElectionsController.new
+ @controller = ElectionController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
-
- def test_index
- get :index
- assert_response :success
- assert_template 'list'
- end
-
- def test_list
- get :list
-
- assert_response :success
- assert_template 'list'
-
- assert_not_nil assigns(:elections)
- end
-
- def test_show
- get :show, :id => 1
-
- assert_response :success
- assert_template 'show'
-
- assert_not_nil assigns(:election)
- assert assigns(:election).valid?
- end
-
- def test_new
- get :new
-
- assert_response :success
- assert_template 'new'
-
- assert_not_nil assigns(:election)
- end
-
- def test_create
- num_elections = Election.count
-
- post :create, :election => {}
-
- assert_response :redirect
- assert_redirected_to :action => 'list'
-
- assert_equal num_elections + 1, Election.count
- end
-
- def test_edit
- get :edit, :id => 1
-
- assert_response :success
- assert_template 'edit'
-
- assert_not_nil assigns(:election)
- assert assigns(:election).valid?
- end
-
- def test_update
- post :update, :id => 1
- assert_response :redirect
- assert_redirected_to :action => 'show', :id => 1
- end
-
- def test_destroy
- assert_not_nil Election.find(1)
-
- post :destroy, :id => 1
- assert_response :redirect
- assert_redirected_to :action => 'list'
-
- assert_raise(ActiveRecord::RecordNotFound) {
- Election.find(1)
- }
+ def test_true
+ #Make rake happy when empty
+ assert true
end
end
require File.dirname(__FILE__) + '/../test_helper'
-require 'graphs_controller'
+require 'graph_controller'
# Re-raise errors caught by the controller.
-class GraphsController; def rescue_action(e) raise e end; end
+class GraphController; def rescue_action(e) raise e end; end
-class GraphsControllerTest < Test::Unit::TestCase
+class GraphControllerTest < Test::Unit::TestCase
#fixtures :data
def setup
- @controller = GraphsController.new
+ @controller = GraphController.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']
+ assert true
end
end
--- /dev/null
+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
+++ /dev/null
-require File.dirname(__FILE__) + '/../test_helper'
-require 'user_controller'
-
-# Re-raise errors caught by the controller.
-class UserController; def rescue_action(e) raise e end; end
-
-class UserControllerTest < Test::Unit::TestCase
- def setup
- @controller = UserController.new
- @request = ActionController::TestRequest.new
- @response = ActionController::TestResponse.new
- end
-
- # Replace this with your real tests.
- def test_truth
- assert true
- end
-end
+++ /dev/null
-require File.dirname(__FILE__) + '/../test_helper'
-
-class FullVoterTest < Test::Unit::TestCase
- fixtures :full_voters
-
- # Replace this with your real tests.
- def test_truth
- assert true
- end
-end
+++ /dev/null
-require File.dirname(__FILE__) + '/../test_helper'
-
-class QuickVoteTest < Test::Unit::TestCase
- fixtures :quick_votes
-
- # Replace this with your real tests.
- def test_truth
- assert true
- end
-end
+++ /dev/null
-require File.dirname(__FILE__) + '/../test_helper'
-
-class QuickVoterTest < Test::Unit::TestCase
- fixtures :quick_voters
-
- # Replace this with your real tests.
- def test_truth
- assert true
- end
-end
+++ /dev/null
-require File.dirname(__FILE__) + '/../test_helper'
-
-class RawVoterListTest < Test::Unit::TestCase
- fixtures :raw_voter_lists
-
- # Replace this with your real tests.
- def test_truth
- assert_kind_of RawVoterList, raw_voter_lists(:first)
- end
-end
--- /dev/null
+require File.dirname(__FILE__) + '/../test_helper'
+require 'selectricity_service_controller'
+
+class SelectricityServiceTest < Test::Unit::TestCase
+ def setup
+ @controller=SelectricityServiceController.new
+ @request=ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_list_quickvotes
+ result= invoke_delegated :vote, :list_quickvotes
+ assert_instance_of Array, result
+ assert_equal result.length, 0
+ end
+ def test_create_quickvote
+ election = ElectionStruct.new :name => "TestVote", :description => "Test Vote", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+ assert_create_quickvote_succeeds election
+ end
+ def test_cast_quickvote
+ test_create_quickvote
+ election = invoke_delegated :vote, :get_quickvote, "TestVote"
+ casted_vote = election.candidate_ids.sort_by {rand} #Shuffles
+ invoke_delegated :vote, :cast_quickvote, "TestVote", 42, [casted_vote]
+ quickvote_votes= invoke_delegated :vote, :get_quickvote_votes, "TestVote"
+ assert_equal quickvote_votes.length, 1
+ assert_equal quickvote_votes[0].vote, casted_vote
+ end
+ def test_cast_mass_quickvote
+ test_create_quickvote
+ election = invoke_delegated :vote, :get_quickvote, "TestVote"
+ 20.times do |t|
+ casted_vote = election.candidate_ids.sort_by {rand}
+ invoke_delegated :vote, :cast_quickvote, "TestVote", t, [casted_vote]
+ end
+ quickvote_votes= invoke_delegated :vote, :get_quickvote_votes, "TestVote"
+ assert_equal quickvote_votes.length, 20
+ end
+ def test_create_mass_quickvote
+ 10.times do |t|
+ election = ElectionStruct.new :name => "test#{t}", :description => "Test Vote", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+ assert_create_quickvote_succeeds election
+ end
+ end
+ def test_create_quickvote_bad_name
+ election = ElectionStruct.new :name => "invalid space", :description => "Test Vote", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+ assert_create_quickvote_fails election
+ end
+ def test_create_quickvote_nil
+ election = ElectionStruct.new
+ assert_create_quickvote_fails election
+ end
+ def test_create_quickvote_name_nil
+ election = ElectionStruct.new :name => "", :description => "Test Vote", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+ assert_create_quickvote_fails election
+ end
+ def test_create_quickvote_description_nil
+ election = ElectionStruct.new :name => "foobar", :description => nil, :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+ assert_create_quickvote_fails election
+
+ end
+ def test_create_quickvote_description_whitespace
+ election = ElectionStruct.new :name => "foobar", :description => " ", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+ assert_create_quickvote_fails election
+ election = ElectionStruct.new :name => "foobar", :description => "\t\t", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+ assert_create_quickvote_fails election
+ end
+ def test_create_quickvote_candidates_nil
+ election = ElectionStruct.new :name => "foobar", :description => "valid", :candidate_names => nil
+ assert_create_quickvote_fails election
+ end
+ def test_create_quickvote_insufficient_candidates
+ election = ElectionStruct.new :name => "foobar", :description => "valid", :candidate_names => ["Apple"]
+ assert_create_quickvote_fails election
+ end
+ def test_create_quickvote_candidates_whitespace
+ election = ElectionStruct.new :name => "foobar", :description => "valid", :candidate_names => [" ", " ", " ", " "]
+ assert_create_quickvote_fails election
+ end
+ def test_create_quickvote_dupe_candidates
+ election = ElectionStruct.new :name => "foobar", :description => "valid", :candidate_names => ["Apple", "Apple", "Apple", "Apple"]
+ assert_create_quickvote_fails election
+
+ # Previous may pass coincidentally if a uniq! then a sizecheck reveals too few unique names
+ # We don't want this to happen. Dupe canidates should fail regardless of how many are left.
+
+ election = ElectionStruct.new :name => "foobar", :description => "valid", :candidate_names => ["Apple", "Apple", "Orange", "Orange", "Pineapple" , "Pineapple"]
+ assert_create_quickvote_fails election
+ end
+ def test_create_quickvote_candidates_nil_mixed
+ election = ElectionStruct.new :name => "foobar", :description => "valid", :candidate_names => ["Apple", nil ]
+ assert_create_quickvote_fails election
+ end
+ def test_create_quickvote_description_xmlescape
+ # Will an embedded XML element bork the table?
+ election = ElectionStruct.new :name => "foobar", :description => "test </string>", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+ assert_create_quickvote_succeeds election
+ end
+ def test_create_quickvote_unprintable_description
+ election = ElectionStruct.new :name => "foobar", :description => "test \x01\x02\x03\x04\x05\x06\x07\x08", :candidate_names => ["Apple", "Orange", "Banana", "Pineapple"]
+ assert_create_quickvote_succeeds election
+ end
+ def test_quickvote_proper_results
+ election = ElectionStruct.new :name => "favdev", :description => "Who is your favorite developer?", :candidate_names => ["mako", "jdong", "justin"]
+ assert_create_quickvote_succeeds election
+ reflection = invoke_delegated :vote, :get_quickvote, "favdev"
+ candidates = {}
+ reflection.candidate_names.each_with_index do |name, index|
+ candidates[name] = reflection.candidate_ids[index]
+ end
+ 25.times do |t|
+ vote = [candidates["jdong"], candidates["mako"], candidates["justin"]]
+ invoke_delegated :vote, :cast_quickvote, "favdev", "1:#{t}", [vote]
+ end
+ 5.times do |t|
+ vote = [candidates["mako"], candidates["justin"], candidates["jdong"]]
+ invoke_delegated :vote, :cast_quickvote, "favdev", "2:#{t}", [vote]
+ end
+ 10.times do |t|
+ vote = [candidates["justin"], candidates["mako"], candidates["jdong"]]
+ invoke_delegated :vote, :cast_quickvote, "favdev", "3:#{t}", [vote]
+ end
+ results=invoke_delegated(:vote, :get_quickvote_results, "favdev")
+ assert_equal results.approval_winners, [candidates["mako"]]
+ assert_equal results.borda_winners, [candidates["jdong"]]
+ assert_equal results.plurality_winners, [candidates["jdong"]]
+ assert_equal results.condorcet_winners, [candidates["jdong"]]
+ assert_equal results.ssd_winners, [candidates["jdong"]]
+ assert_equal results.errors.length, 0
+ end
+ private
+ def assert_create_quickvote_succeeds(election)
+ # Checks if a created quickvote is identical when retrieved
+ old_len=invoke_delegated(:vote,:list_quickvotes).length
+ result = invoke_delegated :vote, :create_quickvote, election
+ assert_equal result, ""
+ reflection = invoke_delegated :vote, :get_quickvote, election.name
+ assert_equal election.description, reflection.description
+ assert_equal 0, election.name.casecmp(reflection.name)
+ assert_equal election.candidate_names, reflection.candidate_names
+ assert_equal(invoke_delegated(:vote,:list_quickvotes).length, old_len+1)
+ end
+ def assert_create_quickvote_fails(election)
+ # Helper function to check that creating this quickvote fails
+ old_len=invoke_delegated(:vote,:list_quickvotes).length
+ result = invoke_delegated :vote, :create_quickvote, election
+ assert_instance_of String, result
+ assert_not_equal result.length, 0
+ assert_equal(invoke_delegated(:vote,:list_quickvotes).length, old_len)
+ end
+end
+++ /dev/null
-require File.dirname(__FILE__) + '/../test_helper'
-
-class VotesTest < Test::Unit::TestCase
- fixtures :votes
-
- # Replace this with your real tests.
- def test_truth
- assert_kind_of Votes, votes(:first)
- end
-end
+++ /dev/null
-require File.dirname(__FILE__) + '/../test_helper'
-require 'voter_notify'
-
-class VoterNotifyTest < Test::Unit::TestCase
- FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
- CHARSET = "utf-8"
-
- include ActionMailer::Quoting
-
- def setup
- ActionMailer::Base.delivery_method = :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries = []
-
- @expected = TMail::Mail.new
- @expected.set_content_type "text", "plain", { "charset" => CHARSET }
- end
-
- private
- def read_fixture(action)
- IO.readlines("#{FIXTURES_PATH}/voter_notify/#{action}")
- end
-
- def encode(subject)
- quoted_printable(subject, CHARSET)
- end
-end
--- /dev/null
+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.
--- /dev/null
+== 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
+
+
--- /dev/null
+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
+
--- /dev/null
+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+
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+ActionView::Base.send :include, SparklinesHelper
--- /dev/null
+
+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
--- /dev/null
+# 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)
+
+ %(<img src="#{ url_for options }" class="#{options[:class] || 'sparkline'}" alt="Sparkline Graph" />)
+ end
+
+end