Allow custimizable graph sizes, to fit with newly modular results page. Also,
[selectricity-live] / app / controllers / graph_controller.rb
1 require 'date'
2 class GraphController < ApplicationController  
3   class GruffGraff
4   
5     def initialize(options)
6       size = options[:size] ? options[:size] : "400x300" #allow custom sizing
7       @graph = options[:graph_type].new(size)
8       
9       @graph.no_data_message = "No Voters"
10       
11       @graph.theme = { :colors => ['#005CD9', '#DC0D13', '#131313', '#990033'],
12                        :background_colors => ['#74CE00', '#FFFFFF'] }
13       @graph.font = File.expand_path('/usr/X11R6/lib/X11/fonts/TTF/Vera.ttf',
14                                    RAILS_ROOT)
15       
16       # fill in the data with the optional data name
17       #Check to see if multiple datasets, if so, fill them all!
18       #Sort by biggest first piece of data.
19       if options[:data].is_a?(Hash) 
20         options[:data].sort {|a,b| b[1][0] <=> a[1][0]}.each do |dataset|
21           @graph.data(dataset[0], dataset[1])
22         end
23       #if each dataset nameless, will have only multiple arrays    
24       elsif options[:data].size > 1 && options[:data].all?  {|i| i.is_a?(Array)}
25         options[:data].each do |array|
26           @graph.data( options.fetch(:data_name, "Data"), array)
27         end
28       else #one dimensional array, just pass it in
29       @graph.data( options.fetch(:data_name, "Data"), options[:data] )
30       end
31       
32       # set the labels or create an empty hash
33       @graph.labels = options[:interval_labels] \
34         if options.has_key?(:interval_labels) and \
35            options[:interval_labels].class == Hash
36       @graph.x_axis_label = options[:x_axis_label] \
37         if options.has_key?(:x_axis_label)
38       @graph.y_axis_label = options[:y_axis_label] \
39         if options.has_key?(:y_axis_label)
40       @graph.title = options[:title] if options.has_key?(:title)
41       
42       @graph.minimum_value = 0.0
43
44     end
45
46     def output
47       return([@graph.to_blob, {:disposition => 'inline', :type => 'image/png'}])
48     end
49
50   end
51
52   # produce a graph of votes per day during an election
53   def votes_per_day
54     @election = Election.find(params[:id])
55     data, labels = get_votes_per_day_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 Per Day",
62                             :x_axis_label => "Data",
63                             :y_axis_label =>"Number of Votes")
64     send_data(*graph.output)
65   end
66   
67   #will place votes in a fixed number of intervals, and shows votes over time
68   def votes_per_interval
69     @election = Election.find(params[:id])
70     data, labels, scale = get_votes_per_interval_data(@election)
71     
72     graph = GruffGraff.new( :graph_type => Gruff::Line,
73                             :data_name => @election.name,
74                             :data => data,
75                             :interval_labels => labels,
76                             :title => "Voters Over Time",
77                             :size => "700x400",
78                             :x_axis_label => scale,
79                             :y_axis_label => "Number of Votes")
80     send_data(*graph.output)
81   end
82   
83   def borda_bar
84     @election = Election.find(params[:id])
85     @election.results unless @election.borda_result
86     data, labels = get_borda_points(@election.borda_result)
87     
88     graph = GruffGraff.new( :graph_type => Gruff::Bar,
89                             :data_name => @election.name,
90                             :data => data,
91                             :interval_labels => labels,
92                             :title => "Points Per Candidate",
93                             :y_axis_label => "Points",
94                             :x_axis_label => "Candidate")
95     send_data(*graph.output)
96   end
97   #Acording to Tufte, small, concomparitive, highly labeled data sets usually
98   #belong in tables. The following is a bar graph...but would it be better
99   #as a table?
100   def choices_positions
101     @election = Election.find(params[:id])
102     legend = Hash.new   
103     alldata, labels = get_positions_info(@election)    
104     @election.results unless @election.condorcet_result || @election.ssd_result
105     ranked_candidates = @election.condorcet_result.ranked_candidates.flatten
106     
107     names = Hash.new
108     candidates = @election.candidates.sort.collect {|candidate| candidate.id}
109     candidates.each do |candidate|
110       names[candidate]= (Candidate.find(candidate)).name
111     end
112     
113     ranked_candidates.each_with_index \
114     {|candidate, index| legend[names[candidate]] = alldata[index]}
115     
116     graph = GruffGraff.new( :graph_type => Gruff::Bar,
117                             :data => legend,
118                             :interval_labels => labels,
119                             :title => "Times Voted in Each Position",
120                             :y_axis_label => "Number of Times Ranked",
121                             :x_axis_label => "Rank")
122     send_data(*graph.output) 
123   end
124   
125   def plurality_pie
126     @election = Election.find(params[:id])
127     @election.results unless @election.plurality_result || @election.approval_result
128     votes = @election.votes.size
129     data = Hash.new
130     names = @election.names_by_id
131     
132     @election.plurality_result.points.each do |candidate, votes|
133       data[names[candidate]] = votes
134     end
135      
136     pie = GruffGraff.new ( :graph_type => Gruff::Pie,
137                            :title => "Percentage of First Place Votes",
138                            :data => data)
139     send_data(*pie.output)
140                            
141   end
142   
143  private 
144   def get_positions_info(election)
145     buckets = Hash.new
146     buckets2= Hash.new
147     rank_labels = Hash.new
148     
149     #attach the ranking to the candidate's array to which is belongs
150     #creating a key if necessary
151     election.votes.each do |vote|
152       vote.rankings.each do |ranking|
153         
154          unless buckets.has_key?(ranking.candidate_id)
155            buckets[ranking.candidate_id] = []
156          end
157         buckets[ranking.candidate_id] << ranking.rank
158         
159       end
160     end
161     
162     #count how many times each candidate has been ranked at a certain level
163     buckets.each_pair do |id, array|
164       (1..election.candidates.size).each do |i|
165         buckets2[id] = [] unless buckets2.has_key?(id)
166         buckets2[id] << (array.find_all {|rank| rank == i}).size
167       end
168     end
169     
170     #sort by amount of 1st place votes
171     sorted_data = buckets2.values.sort {|a,b| b[0] <=> a[0]}
172     
173     election.votes.each do |vote|
174       vote.rankings.size.times do |i|
175         rank_labels[i] = (i+1).to_s
176       end
177     end
178     
179     return sorted_data, rank_labels   
180   end
181    
182   # generate the data and labels for each graph
183   def get_votes_per_day_data(election)
184     voter_days = Array.new
185     unique_days = Array.new
186     total_per_day = Array.new
187     election_days = Hash.new
188     
189     #turn election startdate into date object, and create the range of election
190     startdate = Date.parse(election.startdate.to_s)
191     election_range = startdate..Date.today
192     
193     # create a hash with all the dates of the election in String format
194     # referenced by their order in the election
195     election_range.each_with_index do |day, index|
196       election_days[index] = day.to_s
197     end
198     
199     # Now I need to create an array with all the times votes were made
200     election.votes.each do |vote|
201       next unless vote.time
202       voter_days << Date.parse(vote.time.to_s)
203     end
204     voter_days.sort!
205     
206     # Now I need to count how many times each each date appears in voter_days,
207     # and put that number into a votes_per_day array, the 'data' for the graph    
208     #Create an array of unique days from voter_days
209     voter_days.each do |day|
210       unless unique_days.any? {|date| date.eql?(day)}
211         unique_days << day
212       end
213     end
214     unique_days.sort!
215     
216     #find all dates where those days = date at current index, put size of returned
217     #array into total_per_day
218     unique_days.each_with_index do |date, index|
219       total_per_day << (voter_days.select {|day| day.eql?(date)}).size
220     end    
221
222     # return the data and the labels
223     return total_per_day, election_days
224    
225   end
226   
227   def get_votes_per_interval_data(election)
228     labels_hash = Hash.new
229     buckets = Hash.new
230     total_per_interval = Array.new
231     interval_type = ""
232     
233     starttime = election.startdate
234     timedelta = Time.now - starttime
235     numcols = 10
236     interval_length = timedelta/numcols
237     
238     # Make a hash, buckets, indexed by time intervals and containing empty arrays
239     # The time object must come first in addition! 
240     # i would start at 0, i+1 goes from 1 up till numcols
241     numcols.times {|i| buckets[starttime + ((i+1)*interval_length)] = []}
242      
243     # Put votes into bucket according to the time interval to which they belong,
244     # referenced by their key
245     # Will build a graph over time, as each successive interval will have more
246     # vote objects  
247     election.votes.each do |vote|
248       next unless vote.time
249       buckets.keys.sort.each do |inter|
250         if vote.time < inter
251           buckets[inter] << vote
252         end
253       end
254     end
255   
256     total_per_interval = buckets.keys.sort.collect {|key| buckets[key].size}
257     
258     # Create the hash for the labels. Each graph has ten columns, and three
259     # will be labeled
260     if timedelta < 2.hours #under two hours use minutes for labels
261       labels_hash[0] = starttime.min.to_s
262       labels_hash[(numcols/2)-1] = (starttime + (timedelta/2)).min.to_s
263       labels_hash[numcols-1] = Time.now.min.to_s
264       interval_type = "Minute of the Hour"
265     elsif timedelta < 2.days #more than 2 hours means use hours for labels
266       labels_hash[0] = starttime.hour.to_s
267       labels_hash[(numcols/2)-1] = (starttime + (timedelta/2)).hour.to_s
268       labels_hash[numcols-1] = Time.now.hour.to_s
269       interval_type = "Hour of the Day on 24 hour scale"
270     else #more than 2 days means use dates for labels
271       labels_hash[0] = (Date.parse(starttime.to_s)).to_s
272       labels_hash[(numcols/2)-1] = (Date.parse((starttime + (timedelta/2)).to_s)).to_s
273       labels_hash[numcols-1] = (Date.today).to_s
274       interval_type = "The Date"
275     end
276     
277     # Make sure to return an array for data and hash for labels
278     return total_per_interval, labels_hash, interval_type   
279   end
280   
281   def get_borda_points(result)
282     points = Array.new
283     labels = Hash.new
284
285     #Populate points with an sorted array from election.votes hash
286     #biggest to smallest will go from left to right
287     points = result.points.sort do |a, b|
288       b[1] <=> a[1]
289     end.collect {|i| i[1]}
290
291     #make the labels  
292     result.ranked_candidates.each_with_index do |candidate, index|
293       labels[index] = Candidate.find(candidate).name
294     end
295
296     return points, labels
297   end
298
299   #most vote result objects require an array of vote arrays, which this will make
300   def make_preference_tally(election)
301     preference_tally = Array.new
302     @election.voters.each do |voter|
303       next unless voter.voted?
304       preference_tally << voter.vote.rankings.sort.collect \
305         { |ranking| ranking.candidate.id }
306     end
307   return preference_tally
308   end
309
310 end

Benjamin Mako Hill || Want to submit a patch?