diff --git a/MIT-LICENSE b/MIT-LICENSE new file mode 100644 index 0000000..8eaf6db --- /dev/null +++ b/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2008 [name of plugin creator] + +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. diff --git a/README b/README new file mode 100644 index 0000000..007117e --- /dev/null +++ b/README @@ -0,0 +1,35 @@ +GoogleVisualization +=================== + +This plugin currently covers the motion chart (gap minder) google visualization. You can see an example of one here: http://googlesystem.blogspot.com/2008/03/google-spreadsheets-adds-gadgets.html + + +Example +======= + +In order to use the plugin you simply need to install it into your rails app and then have the following code in a view: + +#Model +class FakeModel < ActiveRecord::Base + #this is a fake model and can be replaced with any object + #I've defined some fake attribute accessors below + attr_accessors :group, :created_at, :x, :y, :z, :more_data_1, :more_data_2 +end + +#Controller +def index + @collection = FakeModel.find(:all) +end + +#View (Here is the important stuff) +<% gap_minder_for(@collection, :width => 500, :height => 300) do |gm| %> + <% gm.label("Department Group") {|collection_item| collection_item.group } %> + <% gm.time("Created At") {|collection_item| collection_item.created_at } %> + <% gm.x("X") {|collection_item| collection_item.x } %> + <% gm.y("Y") {|collection_item| collection_item.y } %> + <% gm.bubble_size("Z") {|collection_item| collection_item.z } %> + <% gm.extra_column("More Data 1") {|collection_item| collection_item.more_data_1 } %> + <% gm.extra_column("More Data 2") {|collection_item| collection_item.more_data_2 } %> +<% end %> + +Copyright (c) 2008 [name of plugin creator], released under the MIT license diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..2e45aea --- /dev/null +++ b/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the google_visualization plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the google_visualization plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'GoogleVisualization' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/init.rb b/init.rb new file mode 100644 index 0000000..a19de55 --- /dev/null +++ b/init.rb @@ -0,0 +1,2 @@ +# Include hook code here +ActionView::Base.send :include, GoogleVisualization::Helpers diff --git a/install.rb b/install.rb new file mode 100644 index 0000000..f7732d3 --- /dev/null +++ b/install.rb @@ -0,0 +1 @@ +# Install hook code here diff --git a/lib/.autotest b/lib/.autotest new file mode 100644 index 0000000..82b3fcf --- /dev/null +++ b/lib/.autotest @@ -0,0 +1,5 @@ +Autotest.add_hook :initialize do |at| + at.add_mapping(%r%.*\.rb$%) { + at.files_matching %r%^spec_.*\.rb$% + } +end diff --git a/lib/google_visualization.rb b/lib/google_visualization.rb new file mode 100644 index 0000000..1da2a93 --- /dev/null +++ b/lib/google_visualization.rb @@ -0,0 +1,142 @@ +# GoogleVisualization +module GoogleVisualization + class GapMinder + + attr_reader :collection, :collection_methods, :options, :size, :helpers, :procedure_hash, :name + + def method_missing(method, *args, &block) + if Mappings.columns.include?(method) + procedure_hash[method] = [args[0], block] + else + helpers.send(method, *args, &block) + end + end + + def initialize(view_instance, collection, options={}, *args) + @helpers = view_instance + @collection = collection + @collection_methods = collection_methods + @options = options.reverse_merge({:width => 600, :height => 300}) + @columns = [] + @rows = [] + @procedure_hash = {:color => ["Department", lambda {|item| label_to_color(@procedure_hash[:label][1].call(item)) }] } + @size = collection.size + @name = "gap_minder_#{self.id.to_s.gsub("-","")}" + @labels = {} + @color_count = 0 + end + + def header + content_tag(:div, "", :id => name, :style => "width: #{options[:width]}px; height: #{options[:height]}px;") + end + + def body + javascript_tag do + "var data = new google.visualization.DataTable();\n" + + "data.addRows(#{size});\n" + + render_columns + + render_rows + + "var #{name} = new google.visualization.MotionChart(document.getElementById('#{name}'));\n" + + "#{name}.draw(data, {width: #{options[:width]}, height: #{options[:height]}});" + end + end + + def render + header + "\n" + body + end + + def render_columns + if required_methods_supplied? + Mappings.columns.each { |c| @columns << gap_minder_add_column(procedure_hash[c]) } + procedure_hash.each { |key, value| @columns << gap_minder_add_column(value) if not Mappings.columns.include?(key) } + @columns.join("\n") + end + end + + def render_rows + if required_methods_supplied? + collection.each_with_index do |item, index| + Mappings.columns.each_with_index {|name,column_index| @rows << gap_minder_set_value(index, column_index, procedure_hash[name][1].call(item)) } + procedure_hash.each {|key,value| @rows << gap_minder_set_value(index, key, procedure_hash[key][1].call(item)) unless Mappings.columns.include?(key) } + end + @rows.join("\n") + end + end + + def required_methods_supplied? + Mappings.columns.each do |key| + unless procedure_hash.has_key? key + raise "GapMinder Must have the #{key} method called before it can be rendered" + end + end + end + + def gap_minder_add_column(title_proc_tuple) + title = title_proc_tuple[0] + procedure = title_proc_tuple[1] + "data.addColumn('#{google_type(procedure)}','#{title}');\n" + end + + def gap_minder_set_value(row, column, value) + "data.setValue(#{row}, #{column}, #{Mappings.ruby_to_javascript_object(value)});\n" + end + + def google_type(procedure) + Mappings.ruby_to_google_type(procedure.call(collection[0]).class) + end + + def google_formatted_value(value) + Mappings.ruby_to_javascript_object(value) + end + + def label_to_color(label) + hashed_label = label.downcase.gsub(" |-","_").to_sym + if @labels.has_key? hashed_label + @labels[hashed_label] + else + @color_count += 1 + @labels[hashed_label] = @color_count + end + end + + def extra_column(title, &block) + procedure_hash[procedure_hash.size] = [title, block] + end + + end + + module Mappings + def self.ruby_to_google_type(type) + type_hash = { + :String => "string", + :Fixnum => "number", + :Float => "number", + :Date => "date", + :Time => "datetime" + } + type_hash[type.to_s.to_sym] + end + + def self.ruby_to_javascript_object(value) + value_hash = { + :String => lambda {|v| "'#{v}'"}, + :Date => lambda {|v| "new Date(#{v.to_s.gsub("-",",")})"}, + :Fixnum => lambda {|v| v }, + :Float => lambda {|v| v } + } + value_hash[value.class.to_s.to_sym].call(value) + end + + def self.columns + [:label, :time, :x, :y, :color, :bubble_size] + end + end + + module Helpers + def gap_minder_for(collection, options={}, *args, &block) + gap_minder = GapMinder.new(self, collection, options) + yield gap_minder + concat(gap_minder.render, block.binding) + end + end +end diff --git a/lib/spec_google_visualization.rb b/lib/spec_google_visualization.rb new file mode 100644 index 0000000..f53f4ac --- /dev/null +++ b/lib/spec_google_visualization.rb @@ -0,0 +1,77 @@ +require 'active_support' +require 'spec' +require 'google_visualization.rb' + +class CollectionFixture + attr_accessor :label, :time, :x, :y, :bubble_size, :extra, :extra_2 + + def initialize(attributes) + attributes.each {|key,value| self.send((key.to_s + "=").to_sym, value)} + end +end + +describe GoogleVisualization do + describe GoogleVisualization::GapMinder do + before do + @collection = [CollectionFixture.new(:label => "Monkey", :time => Date.today, :x => 5, :y => 10, :bubble_size => 50, :extra => 1, :extra_2 => 2)] + + @gap_minder = GoogleVisualization::GapMinder.new(self, @collection) + @gap_minder.label("Department") {|cf| cf.label} + @gap_minder.time("Time of Year") {|cf| cf.time} + @gap_minder.x("X Axis") {|cf| cf.x} + @gap_minder.y("Y Axis") {|cf| cf.y} + @gap_minder.bubble_size("Bubble Size") {|cf| cf.bubble_size} + @gap_minder.extra_column("Extra") {|cf| cf.extra } + @gap_minder.extra_column("Extra 2") {|cf| cf.extra_2 } + + @invalid_gap_minder = GoogleVisualization::GapMinder.new(self, @collection) + end + + it "should build a valid procedure_hash" do + @gap_minder.procedure_hash.should be_instance_of(Hash) + @gap_minder.procedure_hash.should_not be_empty + @gap_minder.procedure_hash.each do |key,value| + @gap_minder.procedure_hash[key].should be_instance_of(Array) + #key.should be_instance_of(Symbol) + value[0].should be_instance_of(String) + value[1].should be_instance_of(Proc) + end + end + + it "should render valid columns" do + puts "\n" + puts @gap_minder.render_columns + end + + it "should render valid rows" do + puts "\n" + puts @gap_minder.render_rows + end + + it "should raise and exception" do + lambda {@invalid_gap_minder.render_columns}.should raise_error + end + + end + + describe GoogleVisualization::Mappings do + it "#ruby_to_google_type should produce the correct types" do + GoogleVisualization::Mappings.ruby_to_google_type(String).should == "string" + GoogleVisualization::Mappings.ruby_to_google_type(Date).should == "date" + GoogleVisualization::Mappings.ruby_to_google_type(Fixnum).should == "number" + GoogleVisualization::Mappings.ruby_to_google_type(Float).should == "number" + GoogleVisualization::Mappings.ruby_to_google_type(Time).should == "datetime" + end + + it "#ruby_to_javascript_object should produce the correct javascript" do + GoogleVisualization::Mappings.ruby_to_javascript_object(Date.parse("2008-01-02")).should == "new Date(2008,01,02)" + GoogleVisualization::Mappings.ruby_to_javascript_object("my string").should == "'my string'" + GoogleVisualization::Mappings.ruby_to_javascript_object(8).should == 8 + GoogleVisualization::Mappings.ruby_to_javascript_object(8.6).should == 8.6 + end + + it "#columns should be a list of symbols" do + GoogleVisualization::Mappings.columns.should be_instance_of Array + end + end +end diff --git a/tasks/google_visualization_tasks.rake b/tasks/google_visualization_tasks.rake new file mode 100644 index 0000000..349c1cb --- /dev/null +++ b/tasks/google_visualization_tasks.rake @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +# task :google_visualization do +# # Task goes here +# end diff --git a/uninstall.rb b/uninstall.rb new file mode 100644 index 0000000..9738333 --- /dev/null +++ b/uninstall.rb @@ -0,0 +1 @@ +# Uninstall hook code here