Fixed minor bug in the scruffy code.
[selectricity-live] / app / controllers / graph_controller.rb
1 require 'date'
2 class GraphController < ApplicationController
3
4   class GruffGraff
5   
6     def initialize(options)
7       size = "700x400"
8       @graph = options[:graph_type].new(size)
9
10       @graph.theme = { :background_colors => ['#73BF26', '#ffffff'] }
11       @graph.font = File.expand_path('/usr/X11R6/lib/X11/fonts/TTF/Vera.ttf',
12                                    RAILS_ROOT)
13       
14       # fill in the data with the optional data name
15       @graph.data( options.fetch(:data_name, nil), options[:data] )
16
17       # set the labels or create an empty hash
18       @graph.labels = options[:interval_labels] \
19         if options.has_key?(:interval_labels) and \
20            options[:interval_labels].class = Hash
21       @graph.x_axis_label = options[:x_axis_label] \
22         if options.has_key?(:x_axis_label)
23       @graph.y_axis_label = options[:y_axis_label] \
24         if options.has_key?(:y_axis_label)
25       @graph.title = options[:title] if options.has_key?(:title)
26       
27       @graph.minimum_value = 0.0
28
29     end
30
31     def output
32       return([@graph.to_blob, {:disposition => 'inline', :type => 'image/png'}])
33     end
34
35   end
36
37   # produce a graph of votes per day during an election
38   def votes_per_day
39     @election = Election.find(params[:id])
40     data, labels = get_votes_per_day_data(@election)
41     
42     graph = GruffGraff.new( :graph_type => Gruff::Line,
43                             :data_name => @election.name,
44                             :data => data,
45                             :interval_labels => labels,
46                             :title => "Voters Per Day",
47                             :x_axis_label => "Data",
48                             :y_axis_label =>"Number of Votes")
49     send_data(*graph.output)
50   end
51   
52   #will place votes in a fixed number of intervals, and shows votes over time
53   def votes_per_interval
54     @election = Election.find(params[:id])
55     data, labels, scale = get_votes_per_interval_data(@election)
56     
57     graph = GruffGraff.new( :graph_type => Gruff::Line,
58                             :data_name => @election.name,
59                             :data => data,
60                             :interval_labels => labels,
61                             :title => "Voters Over Time",
62                             :x_axis_label => scale,
63                             :y_axis_label => "Number of Votes")
64     send_data(*graph.output)
65   end
66  
67   def quickvote_bar
68     @election = Election.find(params[:id])
69   end
70   
71   def borda_bar
72     @election = Election.find(params[:id])
73     pref_tally = make_preference_tally(@election)
74     
75     @borda_result = BordaVote.new(pref_tally).result
76     data, labels = get_borda_points(@borda_result)
77     
78     graph = GruffGraff.new( :graph_type => Gruff::Bar,
79                             :data_name => @election.name,
80                             :data => data,
81                             :interval_labels => labels,
82                             :title => "Points Per Candidate",
83                             :y_axis_label => "Points",
84                             :x_axis_label => "Candidate")
85     send_data(*graph.output)
86   end
87  
88  private 
89    
90   # generate the data and labels for each graph
91   def get_votes_per_day_data(election)
92     voter_days = Array.new
93     unique_days = Array.new
94     total_per_day = Array.new
95     election_days = Hash.new
96     
97     #turn election startdate into date object, and create the range of election
98     startdate = Date.parse(election.startdate.to_s)
99     election_range = startdate..Date.today
100     
101     # create a hash with all the dates of the election in String format
102     # referenced by their order in the election
103     election_range.each_with_index do |day, index|
104       election_days[index] = day.to_s
105     end
106     
107     # Now I need to create an array with all the times votes were made
108     election.votes.each do |vote|
109         voter_days << Date.parse(vote.time.to_s)
110     end
111     voter_days.sort!
112     
113     # Now I need to count how many times each each date appears in voter_days,
114     # and put that number into a votes_per_day array, the 'data' for the graph    
115     #Create an array of unique days from voter_days
116     voter_days.each do |day|
117       unless unique_days.any? {|date| date.eql?(day)}
118         unique_days << day
119       end
120     end
121     unique_days.sort!
122     
123     #find all dates where those days = date at current index, put size of returned
124     #array into total_per_day
125     unique_days.each_with_index do |date, index|
126       total_per_day << (voter_days.select {|day| day.eql?(date)}).size
127     end    
128
129     # return the data and the labels
130     return total_per_day, election_days
131    
132   end
133   
134   def get_votes_per_interval_data(election)
135     labels_hash = Hash.new
136     buckets = Hash.new
137     total_per_interval = Array.new
138     interval_type = ""
139     
140     starttime = election.startdate
141     timedelta = Time.now - starttime
142     numcols = 10
143     interval_length = timedelta/numcols
144     
145     # Make a hash, buckets, indexed by time intervals and containing empty arrays
146     # The time object must come first in addition! 
147     # i would start at 0, i+1 goes from 0 up till numcols
148     numcols.times {|i| buckets[starttime + ((i+1)*interval_length)] = []}
149      
150     # Put votes into bucket according to the time interval to which they belong,
151     # referenced by their key
152     # Will build a graph over time, as each successive interval wil lhave more
153     # vote objects  
154     election.votes.each do |vote|
155       buckets.keys.sort.each do |inter|
156         if vote.time < inter
157           buckets[inter] << vote
158         end
159       end
160     end
161   
162     total_per_interval = buckets.keys.sort.collect {|key| buckets[key].size}
163     
164     # Create the hash for the labels. Each graph has ten columns, and three
165     # will be labeled
166     if timedelta < 2.hours #under two hours use minutes for labels
167       labels_hash[0] = starttime.min.to_s
168       labels_hash[(numcols/2)-1] = (starttime + (timedelta/2)).min.to_s
169       labels_hash[numcols-1] = Time.now.min.to_s
170       interval_type = "Minute of the Hour"
171     elsif timedelta < 2.days #more than 2 hours means use hours for labels
172       labels_hash[0] = starttime.hour.to_s
173       labels_hash[(numcols/2)-1] = (starttime + (timedelta/2)).hour.to_s
174       labels_hash[numcols-1] = Time.now.hour.to_s
175       interval_type = "Hour of the Day on 24 hour scale"
176     else #more than 2 days means use dates for labels
177       labels_hash[0] = (Date.parse(starttime.to_s)).to_s
178       labels_hash[(numcols/2)-1] = (Date.parse(starttime + (timedelta/2))).to_s
179       labels_hash[numcols-1] = (Date.today).to_s
180       interval_type = "The Date"
181     end
182     
183     # Make sure to return an array for data and hash for labels
184     return total_per_interval, labels_hash, interval_type   
185   end
186   
187   def get_borda_points(result)
188     #points holds how mnay points each candidate has received in array form
189     #becasue Gruff::Bar#data takes only an array
190     points = Array.new
191     labels = Hash.new
192
193     #Populate points with an sorted array from election.votes hash
194     #biggest to smallest will go from left to right
195     points = result.election.votes.sort do |a, b|
196       b[1] <=> a[1]
197     end.collect {|i| i[1]}
198
199     #make the labels  
200     result.ranked_candidates.each_with_index do |candidate, index|
201       labels[index] = Candidate.find(candidate).name
202     end
203
204     return points, labels
205   end
206
207   #most vote result objects require an array of vote arrays, which this will make
208   def make_preference_tally(election)
209     preference_tally = Array.new
210     @election.voters.each do |voter|
211       next unless voter.voted?
212       preference_tally << voter.vote.rankings.sort.collect \
213         { |ranking| ranking.candidate.id }
214     end
215   return preference_tally
216   end
217
218 end

Benjamin Mako Hill || Want to submit a patch?