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

Benjamin Mako Hill || Want to submit a patch?