Added Sparklines controller and dependency, see README. Created method and table...
author<jlsharps@mit.edu> <>
Fri, 17 Aug 2007 22:07:59 +0000 (18:07 -0400)
committer<jlsharps@mit.edu> <>
Fri, 17 Aug 2007 22:07:59 +0000 (18:07 -0400)
22 files changed:
1  2 
README
app/controllers/graph_controller.rb
app/controllers/sparklines_controller.rb
app/models/election.rb
app/models/quick_vote.rb
app/views/quickvote/_defeats_list.rhtml
app/views/quickvote/_pref_table.rhtml
app/views/quickvote/_victories_ties.rhtml
app/views/quickvote/results.rhtml
config/environment.rb
lib/rubyvote/condorcet.rb
test/functional/sparklines_controller_test.rb
vendor/plugins/sparklines/MIT-LICENSE
vendor/plugins/sparklines/README
vendor/plugins/sparklines/Rakefile
vendor/plugins/sparklines/about.yml
vendor/plugins/sparklines/generators/sparklines/sparklines_generator.rb
vendor/plugins/sparklines/generators/sparklines/templates/controller.rb
vendor/plugins/sparklines/generators/sparklines/templates/functional_test.rb
vendor/plugins/sparklines/init.rb
vendor/plugins/sparklines/lib/sparklines.rb
vendor/plugins/sparklines/lib/sparklines_helper.rb

diff --cc README
index 88fcd974eec3baf3f11a8dc97c5e31d6bf813648,88fcd974eec3baf3f11a8dc97c5e31d6bf813648..22f3eb7f1e6460d583fec310786cfeccd68230c4
--- 1/README
--- 2/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:
index 8b617da9c19e163db72d3b84b362840d14a370c4,8b617da9c19e163db72d3b84b362840d14a370c4..1bde09e1c4dae465b1221c1321e1ae8a5393db81
@@@ -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)
      
    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]}
  
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..eb5e6a6221bd2734d7ab1bc57c86963dee5e1813
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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
index 3baeef08e16fe95192f9b5810fe6b43c9de4fcba,cf7bba58a2974f409a2828226d4dd43dc2f023ac..d76c13f633cd7ddb7b48ca560deb60d54f266d4a
@@@ -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'
  
      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
++
++
index 698dac3f02af15ad516918de69600c4e0bfe0f48,13e616824c58c0a1f3b3610bf7e0322fd49604c6..c0367d6b002ebc8f3058bc480c64edb61bbf3aa5
@@@ -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
      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 248c2943700d1bdb0e0d9a62e0795d9d37129e30,248c2943700d1bdb0e0d9a62e0795d9d37129e30..0000000000000000000000000000000000000000
deleted file mode 100644,100644
+++ /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.<br />
--<% end -%>
--
--<% tied.each do |tie| -%>
--<%= names[tie[0]]%> tied with <%= names[tie[1]]%><br />
--<% end -%>
index 32f5de4bc81254afef3a36fbae12a82ab331d21b,32f5de4bc81254afef3a36fbae12a82ab331d21b..4576a0fafb8e7971af4499ba88b9501bacb1a2aa
@@@ -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 -%>
      <% 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>
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..7c3506a820f3d9037762b902e4f1a78f3692bbbd
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,17 @@@
++<% 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>
++              
++      
++      
++
index 0cbd6d00e5f84b6091d19ca1bb55848518aa5c26,0cbd6d00e5f84b6091d19ca1bb55848518aa5c26..c49d68201a48e10363c14811cad171a6abe6dd39
@@@ -1,3 -1,3 +1,4 @@@
++<% %>
  <%require 'whois/whois' %>
  <h1>Results</h1>
  
@@@ -175,7 -175,7 +176,7 @@@ by several other names.</p
  <% 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 />
index ba927343d1dc89aca7a36e7ee2b63e0eaadb4623,ba927343d1dc89aca7a36e7ee2b63e0eaadb4623..e071a1437bf25b6c24074331c4f9d0a9efe09cc0
@@@ -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
index f86e59d1295cdc876b9d9f380892452c5f3bab04,f86e59d1295cdc876b9d9f380892452c5f3bab04..7fda91b48073a0308c8040ddba03ac13d8822929
@@@ -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|
          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
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..7d58be406d88ba989a907a17c669552e3ee54c2f
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..8d1b480afffd54261e5f884ef28e9cba86102303
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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.
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..800b508beff8c7171f822fbf3662cbcdc9f2d36c
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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
++
++
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..d3456e4d602652e04be2d79c91834ff5833219ca
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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
++
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..f018110da10896ab7bb591732fc3c5468a51329f
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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+
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..40e8daedafed45cb2ae530138d693de1e3a07818
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..1c69f290525082bfb9c65b50127c4f632c03ea70
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..7d58be406d88ba989a907a17c669552e3ee54c2f
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..c9c2349628b35fac11feff3f6adeb18416c38662
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,1 @@@
++ActionView::Base.send :include, SparklinesHelper
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..4c4e2de094ecc3726c05c9b4b551e0004d450716
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..c8958d3399504f525de4d389c132ba564ea6356e
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -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)
++              
++              %(<img src="#{ url_for options }" class="#{options[:class] || 'sparkline'}" alt="Sparkline Graph" />)
++      end
++
++end

Benjamin Mako Hill || Want to submit a patch?