├── spec ├── dummy │ ├── Gemfile │ ├── log │ │ ├── test.log │ │ ├── server.log │ │ ├── development.log │ │ └── production.log │ ├── lib │ │ └── tasks │ │ │ └── .gitkeep │ ├── public │ │ ├── favicon.ico │ │ ├── stylesheets │ │ │ └── .gitkeep │ │ ├── images │ │ │ └── rails.png │ │ ├── javascripts │ │ │ ├── application.js │ │ │ ├── rails.js │ │ │ └── dragdrop.js │ │ ├── robots.txt │ │ ├── 422.html │ │ ├── 404.html │ │ ├── 500.html │ │ └── index.html │ ├── vendor │ │ └── plugins │ │ │ └── .gitkeep │ ├── app │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── controllers │ │ │ └── application_controller.rb │ │ └── views │ │ │ └── layouts │ │ │ └── application.html.erb │ ├── Gemfile.lock │ ├── config.ru │ ├── config │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── initializers │ │ │ ├── mime_types.rb │ │ │ ├── inflections.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── session_store.rb │ │ │ └── secret_token.rb │ │ ├── database.yml │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ │ ├── application.rb │ │ └── routes.rb │ ├── doc │ │ └── README_FOR_APP │ ├── Rakefile │ ├── test │ │ ├── performance │ │ │ └── browsing_test.rb │ │ └── test_helper.rb │ ├── script │ │ └── rails │ └── db │ │ └── seeds.rb ├── spec_helper.rb ├── turbolinks_tests │ ├── index.html │ ├── old.html │ ├── fixed.html │ ├── broken.html │ └── turbolinks.js ├── google_visualr │ ├── app │ │ └── helpers │ │ │ └── view_helper_spec.rb │ ├── param_helpers_spec.rb │ ├── base_chart_spec.rb │ ├── formatters_spec.rb │ ├── image_charts_spec.rb │ └── data_table_spec.rb └── support │ └── common.rb ├── .rspec ├── .ruby-version ├── .hound.yml ├── .gitignore ├── lib ├── google_visualr │ ├── version.rb │ ├── interactive │ │ ├── gauge.rb │ │ ├── map.rb │ │ ├── sankey.rb │ │ ├── table.rb │ │ ├── geo_map.rb │ │ ├── word_tree.rb │ │ ├── calendar.rb │ │ ├── timeline.rb │ │ ├── tree_map.rb │ │ ├── geo_chart.rb │ │ ├── org_chart.rb │ │ ├── motion_chart.rb │ │ ├── annotation_chart.rb │ │ ├── intensity_map.rb │ │ ├── annotated_time_line.rb │ │ ├── pie_chart.rb │ │ ├── area_chart.rb │ │ ├── bubble_chart.rb │ │ ├── histogram.rb │ │ ├── combo_chart.rb │ │ ├── gantt_chart.rb │ │ ├── stepped_area_chart.rb │ │ ├── candlestick_chart.rb │ │ ├── line_chart.rb │ │ ├── scatter_chart.rb │ │ ├── bar_chart.rb │ │ └── column_chart.rb │ ├── app │ │ ├── railtie.rb │ │ └── helpers │ │ │ └── view_helper.rb │ ├── image │ │ ├── spark_line.rb │ │ ├── line_chart.rb │ │ ├── pie_chart.rb │ │ └── bar_chart.rb │ ├── formatters.rb │ ├── param_helpers.rb │ ├── base_chart.rb │ ├── packages.rb │ └── data_table.rb └── google_visualr.rb ├── Gemfile ├── Rakefile ├── gemfiles ├── 3.2.gemfile ├── 4.0.gemfile ├── 4.1.gemfile ├── 4.0.gemfile.lock ├── 4.1.gemfile.lock └── 3.2.gemfile.lock ├── .travis.yml ├── Appraisals ├── CONTRIBUTING.md ├── google_visualr.gemspec ├── MIT-LICENSE ├── CHANGELOG.md └── README.md /spec/dummy/Gemfile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /spec/dummy/log/test.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.1.3 2 | -------------------------------------------------------------------------------- /spec/dummy/log/server.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/lib/tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/log/development.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/log/production.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/stylesheets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/vendor/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | ruby: 2 | config_file: .ruby-style.yml 3 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | .idea 5 | vendor/bundle 6 | /Gemfile.lock 7 | -------------------------------------------------------------------------------- /lib/google_visualr/version.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | VERSION = "2.5.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | specs: 3 | 4 | PLATFORMS 5 | ruby 6 | 7 | DEPENDENCIES 8 | -------------------------------------------------------------------------------- /spec/dummy/public/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winston/google_visualr/HEAD/spec/dummy/public/images/rails.png -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // Place your application-specific JavaScript functions and classes here 2 | // This file is automatically included by javascript_include_tag :defaults 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "bundler", ">= 1.3.5" 7 | gem "rspec", "~> 2.99.0" 8 | gem "appraisal" 9 | gem "rails", "~> 4.2.1" 10 | end 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Bundler Gem Tasks 2 | require 'bundler' 3 | Bundler::GemHelper.install_tasks 4 | 5 | require 'rspec/core/rake_task' 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task test: :spec 9 | task default: :spec 10 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /spec/dummy/doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. 3 | -------------------------------------------------------------------------------- /spec/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /gemfiles/3.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | group :development do 6 | gem "bundler", ">= 1.3.5" 7 | gem "rspec", "~> 2.99.0" 8 | gem "appraisal" 9 | gem "rails", "~> 3.2.21" 10 | end 11 | 12 | gemspec :path => "../" 13 | -------------------------------------------------------------------------------- /gemfiles/4.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | group :development do 6 | gem "bundler", ">= 1.3.5" 7 | gem "rspec", "~> 2.99.0" 8 | gem "appraisal" 9 | gem "rails", "~> 4.0.13" 10 | end 11 | 12 | gemspec :path => "../" 13 | -------------------------------------------------------------------------------- /gemfiles/4.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | group :development do 6 | gem "bundler", ">= 1.3.5" 7 | gem "rspec", "~> 2.99.0" 8 | gem "appraisal" 9 | gem "rails", "~> 4.1.10" 10 | end 11 | 12 | gemspec :path => "../" 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | bundler_args: --retry=3 --jobs=3 --no-deployment 3 | cache: bundler 4 | sudo: false 5 | 6 | rvm: 7 | - 2.0.0 8 | - 2.1.6 9 | - 2.2.2 10 | 11 | gemfile: 12 | - gemfiles/3.2.gemfile 13 | - gemfiles/4.0.gemfile 14 | - gemfiles/4.1.gemfile 15 | - Gemfile 16 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | require 'rake' 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag :all %> 6 | <%= javascript_include_tag :defaults %> 7 | <%= csrf_meta_tag %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/test/performance/browsing_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rails/performance_test_help' 3 | 4 | # Profiling results for each test method are written to tmp/performance. 5 | class BrowsingTest < ActionDispatch::PerformanceTest 6 | def test_homepage 7 | get '/' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "3.2" do 2 | group :development do 3 | gem "rails", "~> 3.2.21" 4 | end 5 | end 6 | 7 | appraise "4.0" do 8 | group :development do 9 | gem "rails", "~> 4.0.13" 10 | end 11 | end 12 | 13 | appraise "4.1" do 14 | group :development do 15 | gem "rails", "~> 4.1.10" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/dummy/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) 7 | # Mayor.create(:name => 'Daley', :city => cities.first) 8 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/gauge.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/gauge.html 5 | class Gauge < BaseChart 6 | # For Configuration Options, please refer to: 7 | # http://code.google.com/apis/chart/interactive/docs/gallery/gauge.html#Configuration_Options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/map.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/map.html 5 | class Map < BaseChart 6 | # For Configuration Options, please refer to: 7 | # http://code.google.com/apis/chart/interactive/docs/gallery/map.html#Configuration_Options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/sankey.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # https://developers.google.com/chart/interactive/docs/gallery/sankey 5 | class Sankey < BaseChart 6 | # For Configuration Options, please refer to: 7 | # https://developers.google.com/chart/interactive/docs/gallery/sankey#configuration-options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/table.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/table.html 5 | class Table < BaseChart 6 | # For Configuration Options, please refer to: 7 | # http://code.google.com/apis/chart/interactive/docs/gallery/table.html#Configuration_Options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/geo_map.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/geomap.html 5 | class GeoMap < BaseChart 6 | # For Configuration Options, please refer to: 7 | # http://code.google.com/apis/chart/interactive/docs/gallery/geomap.html#Configuration_Options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/word_tree.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # https://developers.google.com/chart/interactive/docs/gallery/wordtree 5 | class WordTree < BaseChart 6 | # For Configuration Options, please refer to: 7 | # https://developers.google.com/chart/interactive/docs/gallery/wordtree#a-simple-example 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/calendar.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # https://developers.google.com/chart/interactive/docs/gallery/calendar 5 | class Calendar < BaseChart 6 | # For Configuration Options, please refer to: 7 | # https://developers.google.com/chart/interactive/docs/gallery/calendar#configuration-options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/timeline.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # https://developers.google.com/chart/interactive/docs/gallery/timeline 5 | class Timeline < BaseChart 6 | # For Configuration Options, please refer to: 7 | # https://developers.google.com/chart/interactive/docs/gallery/timeline#Configuration_Options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/tree_map.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/treemap.html 5 | class TreeMap < BaseChart 6 | # For Configuration Options, please refer to: 7 | # http://code.google.com/apis/chart/interactive/docs/gallery/treemap.html#Configuration_Options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/geo_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/geochart.html 5 | class GeoChart < BaseChart 6 | # For Configuration Options, please refer to: 7 | # http://code.google.com/apis/chart/interactive/docs/gallery/geochart.html#Configuration_Options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/org_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/orgchart.html 5 | class OrgChart < BaseChart 6 | # For Configuration Options, please refer to: 7 | # http://code.google.com/apis/chart/interactive/docs/gallery/orgchart.html#Configuration_Options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | ### Development 4 | 5 | We use [appraisal](https://github.com/thoughtbot/appraisal) to test against 6 | multiple rubies and rails versions. 7 | 8 | Run `appraisal install` to install all dependencies (See files under `gemfiles` 9 | folder). 10 | 11 | Run `appraisal rake` for tests with all versions of Rails. 12 | 13 | Run `appraisal 4.1 rake` for tests with Rails 4.1. 14 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/motion_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/motionchart.html 5 | class MotionChart < BaseChart 6 | # For Configuration Options, please refer to: 7 | # http://code.google.com/apis/chart/interactive/docs/gallery/motionchart.html#Configuration_Options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/app/railtie.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/helpers/view_helper" 2 | 3 | module GoogleVisualr 4 | 5 | module Rails 6 | 7 | class Railtie < ::Rails::Railtie 8 | 9 | initializer "google_visualr" do 10 | ActiveSupport.on_load(:action_controller) do 11 | include GoogleVisualr::Rails::ViewHelper 12 | end 13 | end 14 | 15 | end 16 | 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/annotation_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # https://developers.google.com/chart/interactive/docs/gallery/annotationchart 5 | class AnnotationChart < BaseChart 6 | # For Configuration Options, please refer to: 7 | # https://developers.google.com/chart/interactive/docs/gallery/annotationchart#configuration-options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/intensity_map.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/intensitymap.html 5 | class IntensityMap < BaseChart 6 | # For Configuration Options, please refer to: 7 | # http://code.google.com/apis/chart/interactive/docs/gallery/intensitymap.html#Configuration_Options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/annotated_time_line.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/annotatedtimeline.html 5 | class AnnotatedTimeLine < BaseChart 6 | # For Configuration Options, please refer to: 7 | # http://code.google.com/apis/chart/interactive/docs/gallery/annotatedtimeline.html#Configuration_Options 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/pie_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/piechart.html 5 | class PieChart < BaseChart 6 | include GoogleVisualr::Packages::CoreChart 7 | 8 | # For Configuration Options, please refer to: 9 | # http://code.google.com/apis/chart/interactive/docs/gallery/piechart.html#Configuration_Options 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/area_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/areachart.html 5 | class AreaChart < BaseChart 6 | include GoogleVisualr::Packages::CoreChart 7 | 8 | # For Configuration Options, please refer to: 9 | # http://code.google.com/apis/chart/interactive/docs/gallery/areachart.html#Configuration_Options 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/bubble_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # https://developers.google.com/chart/interactive/docs/gallery/bubblechart 5 | class BubbleChart < BaseChart 6 | include GoogleVisualr::Packages::CoreChart 7 | 8 | # For Configuration Options, please refer to: 9 | # https://developers.google.com/chart/interactive/docs/gallery/bubblechart#Configuration_Options 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/histogram.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # https://developers.google.com/chart/interactive/docs/gallery/histogram 5 | class Histogram < BaseChart 6 | include GoogleVisualr::Packages::CoreChart 7 | 8 | # For Configuration Options, please refer to: 9 | # https://developers.google.com/chart/interactive/docs/gallery/histogram#Configuration_Options 10 | end 11 | 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, :key => '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/combo_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/combochart.html 5 | class ComboChart < BaseChart 6 | include GoogleVisualr::Packages::CoreChart 7 | 8 | # For Configuration Options, please refer to: 9 | # http://code.google.com/apis/chart/interactive/docs/gallery/combochart.html#Configuration_Options 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/gantt_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # https://developers.google.com/chart/interactive/docs/gallery/ganttchart 5 | class GanttChart < BaseChart 6 | 7 | def package_name 8 | "gantt" 9 | end 10 | 11 | # For Configuration Options, please refer to: 12 | # https://developers.google.com/chart/interactive/docs/gallery/ganttchart#configuration-options 13 | end 14 | 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/stepped_area_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # https://developers.google.com/chart/interactive/docs/gallery/steppedareachart 5 | class SteppedAreaChart < BaseChart 6 | include GoogleVisualr::Packages::CoreChart 7 | 8 | # For Configuration Options, please refer to: 9 | # https://developers.google.com/chart/interactive/docs/gallery/steppedareachart#Configuration_Options 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/candlestick_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/candlestickchart.html 5 | class CandlestickChart < BaseChart 6 | include GoogleVisualr::Packages::CoreChart 7 | 8 | # For Configuration Options, please refer to: 9 | # http://code.google.com/apis/chart/interactive/docs/gallery/candlestickchart.html#Configuration_Options 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. 7 | # 8 | # Note: You'll currently still have to declare fixtures explicitly in integration tests 9 | # -- they do not yet inherit this setting 10 | fixtures :all 11 | 12 | # Add more helper methods to be used by all tests here... 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Dummy::Application.config.secret_token = 'd60ddb67fd1ec1a8398c7955513b94592ea310bcbe04be508e8adb4711464031b8cab58a747d2ac660c58c6df487d01d2a5d1df80f8f2d04abbc4c71a98df93e' 8 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/line_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/linechart.html 5 | class LineChart < BaseChart 6 | include GoogleVisualr::Packages::CoreChart 7 | 8 | def package_name 9 | if material 10 | "line" 11 | else 12 | super 13 | end 14 | end 15 | 16 | # For Configuration Options, please refer to: 17 | # http://code.google.com/apis/chart/interactive/docs/gallery/linechart.html#Configuration_Options 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | development: 4 | adapter: sqlite3 5 | database: db/development.sqlite3 6 | pool: 5 7 | timeout: 5000 8 | 9 | # Warning: The database defined as "test" will be erased and 10 | # re-generated from your development database when you run "rake". 11 | # Do not set this db to the same as development or production. 12 | test: 13 | adapter: sqlite3 14 | database: db/test.sqlite3 15 | pool: 5 16 | timeout: 5000 17 | 18 | production: 19 | adapter: sqlite3 20 | database: db/production.sqlite3 21 | pool: 5 22 | timeout: 5000 23 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/scatter_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/scatterchart.html 5 | class ScatterChart < BaseChart 6 | include GoogleVisualr::Packages::CoreChart 7 | 8 | def package_name 9 | if material 10 | "scatter" 11 | else 12 | super 13 | end 14 | end 15 | 16 | # For Configuration Options, please refer to: 17 | # http://code.google.com/apis/chart/interactive/docs/gallery/scatterchart.html#Configuration_Options 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | # Configure Rails Environment 5 | ENV["RAILS_ENV"] = "test" 6 | require File.expand_path("../dummy/config/environment.rb", __FILE__) 7 | 8 | # Load Support Files 9 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 10 | 11 | module JavaScriptHelper 12 | def normalize_javascript(input) 13 | dupped = input.dup 14 | 15 | dupped.gsub!(/\\u003E/i, ">") 16 | dupped.gsub!(/\\u003C/i, "<") 17 | 18 | dupped 19 | end 20 | end 21 | 22 | RSpec.configure do |config| 23 | # some (optional) config here 24 | include JavaScriptHelper 25 | end 26 | -------------------------------------------------------------------------------- /lib/google_visualr/app/helpers/view_helper.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Rails 3 | module ViewHelper 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | # Rails 5 compatibility fix 8 | if respond_to?(:helper_method) 9 | helper_method "render_chart" 10 | end 11 | end 12 | 13 | def render_chart(chart, dom, options={}) 14 | script_tag = options.fetch(:script_tag) { true } 15 | if script_tag 16 | chart.to_js(dom).html_safe 17 | else 18 | html = "" 19 | html << chart.load_js(dom) 20 | html << chart.draw_js(dom) 21 | html.html_safe 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/bar_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/barchart.html 5 | class BarChart < BaseChart 6 | include GoogleVisualr::Packages::CoreChart 7 | 8 | def package_name 9 | if material 10 | "bar" 11 | else 12 | super 13 | end 14 | end 15 | 16 | def chart_name 17 | if material 18 | "Bar" 19 | else 20 | super 21 | end 22 | end 23 | 24 | # For Configuration Options, please refer to: 25 | # http://code.google.com/apis/chart/interactive/docs/gallery/barchart.html#Configuration_Options 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/google_visualr/interactive/column_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Interactive 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/columnchart.html 5 | class ColumnChart < BaseChart 6 | include GoogleVisualr::Packages::CoreChart 7 | 8 | def package_name 9 | if material 10 | "bar" 11 | else 12 | super 13 | end 14 | end 15 | 16 | def chart_name 17 | if material 18 | "Bar" 19 | else 20 | super 21 | end 22 | end 23 | 24 | # For Configuration Options, please refer to: 25 | # http://code.google.com/apis/chart/interactive/docs/gallery/columnchart.html#Configuration_Options 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/turbolinks_tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Different methods for loading a chart with Turbolinks support

8 | You should test these in Firefox. In Chrome, each link click triggers a full page reload if running from localhost (which defeats the purpose of Turbolinks, and doesn't present the bug). 9 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |

We've been notified about this issue and we'll take a look at it shortly.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/google_visualr/app/helpers/view_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "ApplicationController" do 4 | describe "#render_chart" do 5 | it "has method" do 6 | ApplicationController.instance_methods.should include :render_chart 7 | end 8 | 9 | it "includes method in corresponding helper" do 10 | ApplicationController.helpers.methods.should include :render_chart 11 | end 12 | 13 | it "returns html_safe javascript" do 14 | controller = ApplicationController.new 15 | js = controller.render_chart base_chart, "div_chart" 16 | js.should == base_chart_js("div_chart") 17 | end 18 | 19 | it "returns html_safe javascript without script tag" do 20 | controller = ApplicationController.new 21 | js = controller.render_chart base_chart, "div_chart", script_tag: false 22 | js.should == base_chart_js_without_script_tag("div_chart") 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/turbolinks_tests/old.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | home 8 |
9 | 16 | 17 | -------------------------------------------------------------------------------- /google_visualr.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "google_visualr/version" 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "google_visualr" 8 | s.version = GoogleVisualr::VERSION 9 | s.authors = ["Winston Teo"] 10 | s.email = ["winston.yongwei+google_visualr@gmail.com"] 11 | s.homepage = "https://github.com/winston/google_visualr" 12 | s.summary = "A Ruby wrapper around the Google Chart Tools that allows anyone to create the same beautiful charts with just plain Ruby." 13 | s.description = "This Ruby gem, GoogleVisualr, is a wrapper around the Google Chart Tools that allows anyone to create the same beautiful charts with just Ruby; you don't have to write any JavaScript at all." 14 | 15 | s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.md"] 16 | s.test_files = Dir["spec/**/*"] 17 | 18 | s.license = 'MIT' 19 | end 20 | -------------------------------------------------------------------------------- /spec/turbolinks_tests/fixed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | home 8 |
9 | 17 | 18 | -------------------------------------------------------------------------------- /spec/turbolinks_tests/broken.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | home 8 |
9 | 18 | 19 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011 Winston Teo Yong Wei 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 9 | and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 19 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 22 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/google_visualr/image/spark_line.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Image 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/imagesparkline.html 5 | class SparkLine < BaseChart 6 | include GoogleVisualr::Packages::ImageChart 7 | 8 | # For Configuration Options, please refer to: 9 | # http://code.google.com/apis/chart/interactive/docs/gallery/imagesparkline.html 10 | 11 | # Create URI for sparkline. Override parameters by passing in a hash. 12 | # (see http://code.google.com/apis/chart/image/docs/chart_params.html) 13 | # 14 | # Parameters: 15 | # *params [Optional] Hash of url query parameters 16 | def uri(params = {}) 17 | query_params = {} 18 | 19 | # Chart type: line 20 | query_params[:cht] = "ls" 21 | 22 | # showValueLabels (works as long as :chxt => "x,y") 23 | labels = "0:||" 24 | if @options["showValueLabels"] == false || !@options["showAxisLines"] 25 | labels += "1:||" 26 | end 27 | 28 | query_params[:chxl] = labels 29 | 30 | chart_image_url(query_params.merge(params)) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers. 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Raise an error on page load if there are pending migrations 26 | config.active_record.migration_error = :page_load 27 | 28 | # Debug mode disables concatenation and preprocessing of assets. 29 | config.assets.debug = true 30 | end 31 | -------------------------------------------------------------------------------- /lib/google_visualr/image/line_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Image 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/imagelinechart.html 5 | class LineChart < BaseChart 6 | include GoogleVisualr::Packages::ImageChart 7 | 8 | # For Configuration Options, please refer to: 9 | # http://code.google.com/apis/chart/interactive/docs/gallery/imagelinechart.html 10 | 11 | # Create URI for image line chart. Override parameters by passing in a hash. 12 | # (see http://code.google.com/apis/chart/image/docs/chart_params.html) 13 | # 14 | # Parameters: 15 | # *params [Optional] Hash of url query parameters 16 | def uri(params = {}) 17 | query_params = {} 18 | 19 | # Chart type: line 20 | query_params[:cht] = "lc" 21 | 22 | # showAxisLines 23 | if @options["showAxisLines"] == false 24 | query_params[:cht] = "lc:nda" 25 | end 26 | 27 | # showCategoryLabels (works as long as :chxt => "x,y") 28 | labels = "" 29 | if @options["showCategoryLabels"] == false 30 | labels = "0:||" 31 | else 32 | labels = "0:|" + data_table.get_column(0).join('|') + "|" 33 | end 34 | 35 | # showValueLabels (works as long as :chxt => "x,y") 36 | if @options["showValueLabels"] == false 37 | labels += "1:||" 38 | end 39 | 40 | query_params[:chxl] = labels unless labels.blank? 41 | 42 | chart_image_url(query_params.merge(params)) 43 | end 44 | end 45 | 46 | end 47 | end -------------------------------------------------------------------------------- /lib/google_visualr/image/pie_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Image 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/imagepiechart.html 5 | class PieChart < BaseChart 6 | include GoogleVisualr::Packages::ImageChart 7 | 8 | # For Configuration Options, please refer to: 9 | # http://code.google.com/apis/chart/interactive/docs/gallery/imagepiechart.html 10 | 11 | # Create URI for image pie chart. Override parameters by passing in a hash. 12 | # (see http://code.google.com/apis/chart/image/docs/chart_params.html) 13 | # 14 | # Parameters: 15 | # *params [Optional] Hash of url query parameters 16 | def uri(params = {}) 17 | query_params = {} 18 | 19 | # Chart Type: normal or 3D 20 | query_params[:cht] = @options["is3D"] ? "p3" : "p" 21 | 22 | # Legend (override generic image chart behavior) 23 | query_params[:chdl] = @data_table.get_column(0).join('|') 24 | 25 | # Labels 26 | case options["labels"] 27 | when "name" 28 | query_params[:chl] = @data_table.get_column(0).join('|') 29 | when "value" 30 | query_params[:chl] = @data_table.get_column(1).join('|') 31 | else 32 | query_params[:chl] = "" 33 | end 34 | 35 | # data (override generic chart behavior) 36 | query_params[:chd] = "t:" + @data_table.get_column(1).join(',') 37 | 38 | # Chart Colors (override generic chart default) 39 | query_params[:chco] = @options["colors"].join('|').gsub(/#/, '') if @options["colors"] 40 | 41 | chart_image_url(query_params.merge(params)) 42 | end 43 | 44 | end 45 | 46 | end 47 | end -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require 6 | require "google_visualr" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Settings in config/environments/* take precedence over those specified here. 11 | # Application configuration should go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded. 13 | 14 | # Custom directories with classes and modules you want to be autoloadable. 15 | # config.autoload_paths += %W(#{config.root}/extras) 16 | 17 | # Only load the plugins named here, in the order given (default is alphabetical). 18 | # :all can be used as a placeholder for all plugins not explicitly named. 19 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 20 | 21 | # Activate observers that should always be running. 22 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 23 | 24 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 25 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 26 | # config.time_zone = 'Central Time (US & Canada)' 27 | 28 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 29 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 30 | # config.i18n.default_locale = :de 31 | 32 | # Configure the default encoding used in templates for Ruby 1.9. 33 | config.encoding = "utf-8" 34 | 35 | # Configure sensitive parameters which will be filtered from the log file. 36 | config.filter_parameters += [:password] 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/google_visualr/image/bar_chart.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Image 3 | 4 | # http://code.google.com/apis/chart/interactive/docs/gallery/imagebarchart.html 5 | class BarChart < BaseChart 6 | include GoogleVisualr::Packages::ImageChart 7 | 8 | # For Configuration Options, please refer to: 9 | # http://code.google.com/apis/chart/interactive/docs/gallery/imagebarchart.html 10 | 11 | # Create URI for image bar chart. Override parameters by passing in a hash. 12 | # (see http://code.google.com/apis/chart/image/docs/chart_params.html) 13 | # 14 | # Parameters: 15 | # *params [Optional] Hash of url query parameters 16 | def uri(params = {}) 17 | query_params = {} 18 | 19 | # isStacked/isVertical, Chart Type 20 | chart_type = "b" 21 | chart_type += @options["isVertical"] ? "v" : "h" 22 | chart_type += @options["isStacked"] == false ? "g" : "s" 23 | query_params[:cht] = chart_type 24 | 25 | # showCategoryLabels (works as long as :chxt => "x,y") 26 | labels = "" 27 | val_column = @options["isVertical"] ? 1 : 0 28 | cat_column = @options["isVertical"] ? 0 : 1 29 | if @options["showCategoryLabels"] == false 30 | labels = "#{cat_column}:||" 31 | else 32 | labels = "#{cat_column}:|" + data_table.get_column(0).join('|') + "|" 33 | end 34 | 35 | # showValueLabels (works as long as :chxt => "x,y") 36 | if @options["showValueLabels"] == false 37 | labels += "#{val_column}:||" 38 | end 39 | 40 | query_params[:chxl] = labels unless labels.blank? 41 | 42 | chart_image_url(query_params.merge(params)) 43 | end 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Disable serving static files from the `/public` folder by default since 16 | # Apache or NGINX already handles this. 17 | if Rails.version >= "4.2.0" 18 | config.serve_static_files = true 19 | else 20 | config.serve_static_assets = true 21 | end 22 | config.static_cache_control = "public, max-age=3600" 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | 28 | # Raise exceptions instead of rendering exception templates. 29 | config.action_dispatch.show_exceptions = false 30 | 31 | # Disable request forgery protection in test environment. 32 | config.action_controller.allow_forgery_protection = false 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | config.action_mailer.delivery_method = :test 38 | 39 | # Print deprecation notices to the stderr. 40 | config.active_support.deprecation = :stderr 41 | end 42 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | # The priority is based upon order of creation: 3 | # first created -> highest priority. 4 | 5 | # Sample of regular route: 6 | # match 'products/:id' => 'catalog#view' 7 | # Keep in mind you can assign values other than :controller and :action 8 | 9 | # Sample of named route: 10 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase 11 | # This route can be invoked with purchase_url(:id => product.id) 12 | 13 | # Sample resource route (maps HTTP verbs to controller actions automatically): 14 | # resources :products 15 | 16 | # Sample resource route with options: 17 | # resources :products do 18 | # member do 19 | # get 'short' 20 | # post 'toggle' 21 | # end 22 | # 23 | # collection do 24 | # get 'sold' 25 | # end 26 | # end 27 | 28 | # Sample resource route with sub-resources: 29 | # resources :products do 30 | # resources :comments, :sales 31 | # resource :seller 32 | # end 33 | 34 | # Sample resource route with more complex sub-resources 35 | # resources :products do 36 | # resources :comments 37 | # resources :sales do 38 | # get 'recent', :on => :collection 39 | # end 40 | # end 41 | 42 | # Sample resource route within a namespace: 43 | # namespace :admin do 44 | # # Directs /admin/products/* to Admin::ProductsController 45 | # # (app/controllers/admin/products_controller.rb) 46 | # resources :products 47 | # end 48 | 49 | # You can have the root of your site routed with "root" 50 | # just remember to delete public/index.html. 51 | # root :to => "welcome#index" 52 | 53 | # See how all your routes lay out with "rake routes" 54 | 55 | # This is a legacy wild controller route that's not recommended for RESTful applications. 56 | # Note: This route will make all actions in every controller accessible via GET requests. 57 | # match ':controller(/:action(/:id(.:format)))' 58 | end 59 | -------------------------------------------------------------------------------- /lib/google_visualr/formatters.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | 3 | # http://code.google.com/apis/chart/interactive/docs/reference.html#formatters 4 | class Formatter 5 | include GoogleVisualr::ParamHelpers 6 | 7 | def initialize(options={}) 8 | @options = options 9 | end 10 | 11 | 12 | def columns(*columns) 13 | @columns = columns.flatten 14 | end 15 | 16 | def options(*options) 17 | @options = stringify_keys!(options.pop) 18 | end 19 | 20 | def to_js(&block) 21 | js = "\nvar formatter = new google.visualization.#{self.class.to_s.split('::').last}(" 22 | js << js_parameters(@options) 23 | js << ");" 24 | 25 | yield js if block_given? 26 | 27 | @columns.each do |column| 28 | js << "\nformatter.format(data_table, #{column});" 29 | end 30 | 31 | js 32 | end 33 | 34 | end 35 | 36 | class ArrowFormat < Formatter 37 | end 38 | 39 | class BarFormat < Formatter 40 | end 41 | 42 | class ColorFormat < Formatter 43 | 44 | attr_accessor :ranges 45 | attr_accessor :gradient_ranges 46 | 47 | def initialize 48 | @ranges = Array.new 49 | @gradient_ranges = Array.new 50 | end 51 | 52 | def add_range(from, to, color, bgcolor) 53 | @ranges << { :from => from, :to => to, :color => color, :bgcolor => bgcolor } 54 | end 55 | 56 | def add_gradient_range(from, to, color, fromBgColor, toBgColor) 57 | @gradient_ranges << { :from => from, :to => to, :color => color, :fromBgColor => fromBgColor, :toBgColor => toBgColor } 58 | end 59 | 60 | def to_js 61 | super do |js| 62 | @ranges.each do |r| 63 | js << "\nformatter.addRange(#{typecast(r[:from])}, #{typecast(r[:to])}, #{typecast(r[:color])}, #{typecast(r[:bgcolor])});" 64 | end 65 | @gradient_ranges.each do |r| 66 | js << "\nformatter.addGradientRange(#{typecast(r[:from])}, #{typecast(r[:to])}, #{typecast(r[:color])}, #{typecast(r[:fromBgColor])}, #{typecast(r[:toBgColor])});" 67 | end 68 | end 69 | end 70 | end 71 | 72 | class DateFormat < Formatter 73 | end 74 | 75 | class NumberFormat < Formatter 76 | end 77 | 78 | end -------------------------------------------------------------------------------- /lib/google_visualr/param_helpers.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | 3 | module ParamHelpers 4 | 5 | def stringify_keys!(options) 6 | options.keys.each do |key| 7 | options[key.to_s] = options.delete(key) 8 | end 9 | options 10 | end 11 | 12 | def js_parameters(options) 13 | return "" if options.nil? 14 | 15 | attributes = options.collect { |(key, value)| "#{key}: #{typecast(value)}" } 16 | "{" + attributes.join(", ") + "}" 17 | end 18 | 19 | # If the column type is 'string' , the value should be a string. 20 | # If the column type is 'number' , the value should be a number. 21 | # If the column type is 'boolean' , the value should be a boolean. 22 | # If the column type is 'date' , the value should be a Date object. 23 | # If the column type is 'datetime' , the value should be a DateTime or Time object. 24 | # If the column type is 'timeofday' , the value should be an array of three or four numbers: [hour, minute, second, optional milliseconds]. 25 | # Returns an array of strings if given an array 26 | # Returns 'null' when value is nil. 27 | # Recursive typecasting when value is a hash. 28 | def typecast(value, type = nil) 29 | case 30 | when value.is_a?(String) 31 | return value.to_json 32 | when value.is_a?(Integer) || value.is_a?(Float) 33 | return value 34 | when value.is_a?(TrueClass) || value.is_a?(FalseClass) 35 | return "#{value}" 36 | when value.is_a?(DateTime) || value.is_a?(Time) 37 | if type == "time" 38 | return "new Date(0, 0, 0, #{value.hour}, #{value.min}, #{value.sec})" 39 | else 40 | return "new Date(#{value.year}, #{value.month-1}, #{value.day}, #{value.hour}, #{value.min}, #{value.sec})" 41 | end 42 | when value.is_a?(Date) 43 | return "new Date(#{value.year}, #{value.month-1}, #{value.day})" 44 | when value.nil? 45 | return "null" 46 | when value.is_a?(Array) 47 | return "[" + value.map{|v| typecast(v) }.join(',') + "]" 48 | when value.is_a?(Hash) 49 | return js_parameters(value) 50 | else 51 | return value 52 | end 53 | end 54 | 55 | end 56 | 57 | end -------------------------------------------------------------------------------- /lib/google_visualr.rb: -------------------------------------------------------------------------------- 1 | lib_path = File.dirname(__FILE__) 2 | 3 | # Common 4 | require "#{lib_path}/google_visualr/param_helpers" 5 | 6 | # Base Classes 7 | require "#{lib_path}/google_visualr/data_table" 8 | 9 | require "#{lib_path}/google_visualr/packages" 10 | require "#{lib_path}/google_visualr/base_chart" 11 | 12 | require "#{lib_path}/google_visualr/formatters" 13 | 14 | # Interactive Charts 15 | 16 | ## Main 17 | require "#{lib_path}/google_visualr/interactive/annotated_time_line" 18 | require "#{lib_path}/google_visualr/interactive/annotation_chart" 19 | require "#{lib_path}/google_visualr/interactive/area_chart" 20 | require "#{lib_path}/google_visualr/interactive/bar_chart" 21 | require "#{lib_path}/google_visualr/interactive/bubble_chart" 22 | require "#{lib_path}/google_visualr/interactive/calendar" 23 | require "#{lib_path}/google_visualr/interactive/candlestick_chart" 24 | require "#{lib_path}/google_visualr/interactive/column_chart" 25 | require "#{lib_path}/google_visualr/interactive/combo_chart" 26 | require "#{lib_path}/google_visualr/interactive/gantt_chart" 27 | require "#{lib_path}/google_visualr/interactive/gauge" 28 | require "#{lib_path}/google_visualr/interactive/geo_chart" 29 | require "#{lib_path}/google_visualr/interactive/geo_map" 30 | require "#{lib_path}/google_visualr/interactive/histogram" 31 | require "#{lib_path}/google_visualr/interactive/intensity_map" 32 | require "#{lib_path}/google_visualr/interactive/line_chart" 33 | require "#{lib_path}/google_visualr/interactive/map" 34 | require "#{lib_path}/google_visualr/interactive/motion_chart" 35 | require "#{lib_path}/google_visualr/interactive/org_chart" 36 | require "#{lib_path}/google_visualr/interactive/pie_chart" 37 | require "#{lib_path}/google_visualr/interactive/sankey" 38 | require "#{lib_path}/google_visualr/interactive/scatter_chart" 39 | require "#{lib_path}/google_visualr/interactive/stepped_area_chart" 40 | require "#{lib_path}/google_visualr/interactive/table" 41 | require "#{lib_path}/google_visualr/interactive/timeline" 42 | require "#{lib_path}/google_visualr/interactive/tree_map" 43 | require "#{lib_path}/google_visualr/interactive/word_tree" 44 | 45 | # Image Charts 46 | require "#{lib_path}/google_visualr/image/spark_line" 47 | require "#{lib_path}/google_visualr/image/bar_chart" 48 | require "#{lib_path}/google_visualr/image/line_chart" 49 | require "#{lib_path}/google_visualr/image/pie_chart" 50 | 51 | 52 | # Rails Helper 53 | require "#{lib_path}/google_visualr/app/railtie.rb" if defined?(Rails) 54 | -------------------------------------------------------------------------------- /gemfiles/4.0.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | google_visualr (2.4.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | actionmailer (4.0.13) 10 | actionpack (= 4.0.13) 11 | mail (~> 2.5, >= 2.5.4) 12 | actionpack (4.0.13) 13 | activesupport (= 4.0.13) 14 | builder (~> 3.1.0) 15 | erubis (~> 2.7.0) 16 | rack (~> 1.5.2) 17 | rack-test (~> 0.6.2) 18 | activemodel (4.0.13) 19 | activesupport (= 4.0.13) 20 | builder (~> 3.1.0) 21 | activerecord (4.0.13) 22 | activemodel (= 4.0.13) 23 | activerecord-deprecated_finders (~> 1.0.2) 24 | activesupport (= 4.0.13) 25 | arel (~> 4.0.0) 26 | activerecord-deprecated_finders (1.0.4) 27 | activesupport (4.0.13) 28 | i18n (~> 0.6, >= 0.6.9) 29 | minitest (~> 4.2) 30 | multi_json (~> 1.3) 31 | thread_safe (~> 0.1) 32 | tzinfo (~> 0.3.37) 33 | appraisal (2.0.1) 34 | activesupport (>= 3.2.21) 35 | bundler 36 | rake 37 | thor (>= 0.14.0) 38 | arel (4.0.2) 39 | builder (3.1.4) 40 | diff-lcs (1.2.5) 41 | erubis (2.7.0) 42 | i18n (0.7.0) 43 | mail (2.6.3) 44 | mime-types (>= 1.16, < 3) 45 | mime-types (2.6.1) 46 | minitest (4.7.5) 47 | multi_json (1.11.0) 48 | rack (1.5.3) 49 | rack-test (0.6.3) 50 | rack (>= 1.0) 51 | rails (4.0.13) 52 | actionmailer (= 4.0.13) 53 | actionpack (= 4.0.13) 54 | activerecord (= 4.0.13) 55 | activesupport (= 4.0.13) 56 | bundler (>= 1.3.0, < 2.0) 57 | railties (= 4.0.13) 58 | sprockets-rails (~> 2.0) 59 | railties (4.0.13) 60 | actionpack (= 4.0.13) 61 | activesupport (= 4.0.13) 62 | rake (>= 0.8.7) 63 | thor (>= 0.18.1, < 2.0) 64 | rake (10.4.2) 65 | rspec (2.99.0) 66 | rspec-core (~> 2.99.0) 67 | rspec-expectations (~> 2.99.0) 68 | rspec-mocks (~> 2.99.0) 69 | rspec-core (2.99.2) 70 | rspec-expectations (2.99.2) 71 | diff-lcs (>= 1.1.3, < 2.0) 72 | rspec-mocks (2.99.3) 73 | sprockets (3.1.0) 74 | rack (~> 1.0) 75 | sprockets-rails (2.3.1) 76 | actionpack (>= 3.0) 77 | activesupport (>= 3.0) 78 | sprockets (>= 2.8, < 4.0) 79 | thor (0.19.1) 80 | thread_safe (0.3.5) 81 | tzinfo (0.3.44) 82 | 83 | PLATFORMS 84 | ruby 85 | 86 | DEPENDENCIES 87 | appraisal 88 | bundler (>= 1.3.5) 89 | google_visualr! 90 | rails (~> 4.0.13) 91 | rspec (~> 2.99.0) 92 | -------------------------------------------------------------------------------- /gemfiles/4.1.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | google_visualr (2.4.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | actionmailer (4.1.10) 10 | actionpack (= 4.1.10) 11 | actionview (= 4.1.10) 12 | mail (~> 2.5, >= 2.5.4) 13 | actionpack (4.1.10) 14 | actionview (= 4.1.10) 15 | activesupport (= 4.1.10) 16 | rack (~> 1.5.2) 17 | rack-test (~> 0.6.2) 18 | actionview (4.1.10) 19 | activesupport (= 4.1.10) 20 | builder (~> 3.1) 21 | erubis (~> 2.7.0) 22 | activemodel (4.1.10) 23 | activesupport (= 4.1.10) 24 | builder (~> 3.1) 25 | activerecord (4.1.10) 26 | activemodel (= 4.1.10) 27 | activesupport (= 4.1.10) 28 | arel (~> 5.0.0) 29 | activesupport (4.1.10) 30 | i18n (~> 0.6, >= 0.6.9) 31 | json (~> 1.7, >= 1.7.7) 32 | minitest (~> 5.1) 33 | thread_safe (~> 0.1) 34 | tzinfo (~> 1.1) 35 | appraisal (2.0.1) 36 | activesupport (>= 3.2.21) 37 | bundler 38 | rake 39 | thor (>= 0.14.0) 40 | arel (5.0.1.20140414130214) 41 | builder (3.2.2) 42 | diff-lcs (1.2.5) 43 | erubis (2.7.0) 44 | i18n (0.7.0) 45 | json (1.8.2) 46 | mail (2.6.3) 47 | mime-types (>= 1.16, < 3) 48 | mime-types (2.6.1) 49 | minitest (5.7.0) 50 | rack (1.5.3) 51 | rack-test (0.6.3) 52 | rack (>= 1.0) 53 | rails (4.1.10) 54 | actionmailer (= 4.1.10) 55 | actionpack (= 4.1.10) 56 | actionview (= 4.1.10) 57 | activemodel (= 4.1.10) 58 | activerecord (= 4.1.10) 59 | activesupport (= 4.1.10) 60 | bundler (>= 1.3.0, < 2.0) 61 | railties (= 4.1.10) 62 | sprockets-rails (~> 2.0) 63 | railties (4.1.10) 64 | actionpack (= 4.1.10) 65 | activesupport (= 4.1.10) 66 | rake (>= 0.8.7) 67 | thor (>= 0.18.1, < 2.0) 68 | rake (10.4.2) 69 | rspec (2.99.0) 70 | rspec-core (~> 2.99.0) 71 | rspec-expectations (~> 2.99.0) 72 | rspec-mocks (~> 2.99.0) 73 | rspec-core (2.99.2) 74 | rspec-expectations (2.99.2) 75 | diff-lcs (>= 1.1.3, < 2.0) 76 | rspec-mocks (2.99.3) 77 | sprockets (3.1.0) 78 | rack (~> 1.0) 79 | sprockets-rails (2.3.1) 80 | actionpack (>= 3.0) 81 | activesupport (>= 3.0) 82 | sprockets (>= 2.8, < 4.0) 83 | thor (0.19.1) 84 | thread_safe (0.3.5) 85 | tzinfo (1.2.2) 86 | thread_safe (~> 0.1) 87 | 88 | PLATFORMS 89 | ruby 90 | 91 | DEPENDENCIES 92 | appraisal 93 | bundler (>= 1.3.5) 94 | google_visualr! 95 | rails (~> 4.1.10) 96 | rspec (~> 2.99.0) 97 | -------------------------------------------------------------------------------- /spec/google_visualr/param_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "GoogleVisualr::ParamsHelper" do 4 | module GoogleVisualr 5 | class Mock 6 | include GoogleVisualr::ParamHelpers 7 | end 8 | end 9 | 10 | before do 11 | @klass = GoogleVisualr::Mock.new 12 | end 13 | 14 | describe "#stringify" do 15 | it "converts symbol keys to string keys" do 16 | options = @klass.stringify_keys!({:a => 1}) 17 | options.should == {"a" => 1} 18 | end 19 | end 20 | 21 | describe "#js_parameters" do 22 | it "converts params to be js-ready string" do 23 | options = @klass.js_parameters({:a => 1}) 24 | options.should == "{a: 1}" 25 | 26 | options = @klass.js_parameters({:a => {:b=> 1}}) 27 | options.should == "{a: {b: 1}}" 28 | end 29 | 30 | it "returns empty string for nil options" do 31 | options = @klass.js_parameters(nil) 32 | options.should == "" 33 | end 34 | end 35 | 36 | describe "#typecast" do 37 | def assert_equal(input, expected, type = nil) 38 | options = @klass.typecast(input, type) 39 | options.should == expected 40 | end 41 | 42 | it "returns string" do 43 | assert_equal("I'm \"AWESOME\"", "I'm \"AWESOME\"".to_json) 44 | end 45 | 46 | it "returns number" do 47 | assert_equal(1, 1) 48 | end 49 | 50 | it "returns boolean" do 51 | assert_equal(true , "true" ) 52 | assert_equal(false, "false") 53 | end 54 | 55 | it "returns datetime" do 56 | date = DateTime.now 57 | expected = "new Date(#{date.year}, #{date.month-1}, #{date.day}, #{date.hour}, #{date.min}, #{date.sec})" 58 | assert_equal(date, expected) 59 | end 60 | 61 | it "returns time" do 62 | date = Time.now 63 | expected = "new Date(#{date.year}, #{date.month-1}, #{date.day}, #{date.hour}, #{date.min}, #{date.sec})" 64 | assert_equal(date, expected) 65 | end 66 | 67 | it "returns time, if specified" do 68 | date = Time.now 69 | expected = "new Date(0, 0, 0, #{date.hour}, #{date.min}, #{date.sec})" 70 | assert_equal(date, expected, "time") 71 | end 72 | 73 | it "returns date" do 74 | date = Date.today 75 | expected = "new Date(#{date.year}, #{date.month-1}, #{date.day})" 76 | assert_equal(date, expected) 77 | end 78 | 79 | it "returns null" do 80 | assert_equal(nil, "null") 81 | end 82 | 83 | it "returns array of strings" do 84 | assert_equal({:colors => ['a', 'b']}, "{colors: [\"a\",\"b\"]}") 85 | end 86 | 87 | it "recursively calls js_parameters" do 88 | assert_equal({:a => {:b => {:c => 1}}}, "{a: {b: {c: 1}}}") 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /gemfiles/3.2.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | google_visualr (2.4.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | actionmailer (3.2.21) 10 | actionpack (= 3.2.21) 11 | mail (~> 2.5.4) 12 | actionpack (3.2.21) 13 | activemodel (= 3.2.21) 14 | activesupport (= 3.2.21) 15 | builder (~> 3.0.0) 16 | erubis (~> 2.7.0) 17 | journey (~> 1.0.4) 18 | rack (~> 1.4.5) 19 | rack-cache (~> 1.2) 20 | rack-test (~> 0.6.1) 21 | sprockets (~> 2.2.1) 22 | activemodel (3.2.21) 23 | activesupport (= 3.2.21) 24 | builder (~> 3.0.0) 25 | activerecord (3.2.21) 26 | activemodel (= 3.2.21) 27 | activesupport (= 3.2.21) 28 | arel (~> 3.0.2) 29 | tzinfo (~> 0.3.29) 30 | activeresource (3.2.21) 31 | activemodel (= 3.2.21) 32 | activesupport (= 3.2.21) 33 | activesupport (3.2.21) 34 | i18n (~> 0.6, >= 0.6.4) 35 | multi_json (~> 1.0) 36 | appraisal (2.0.1) 37 | activesupport (>= 3.2.21) 38 | bundler 39 | rake 40 | thor (>= 0.14.0) 41 | arel (3.0.3) 42 | builder (3.0.4) 43 | diff-lcs (1.2.5) 44 | erubis (2.7.0) 45 | hike (1.2.3) 46 | i18n (0.7.0) 47 | journey (1.0.4) 48 | json (1.8.2) 49 | mail (2.5.4) 50 | mime-types (~> 1.16) 51 | treetop (~> 1.4.8) 52 | mime-types (1.25.1) 53 | multi_json (1.11.0) 54 | polyglot (0.3.5) 55 | rack (1.4.5) 56 | rack-cache (1.2) 57 | rack (>= 0.4) 58 | rack-ssl (1.3.4) 59 | rack 60 | rack-test (0.6.3) 61 | rack (>= 1.0) 62 | rails (3.2.21) 63 | actionmailer (= 3.2.21) 64 | actionpack (= 3.2.21) 65 | activerecord (= 3.2.21) 66 | activeresource (= 3.2.21) 67 | activesupport (= 3.2.21) 68 | bundler (~> 1.0) 69 | railties (= 3.2.21) 70 | railties (3.2.21) 71 | actionpack (= 3.2.21) 72 | activesupport (= 3.2.21) 73 | rack-ssl (~> 1.3.2) 74 | rake (>= 0.8.7) 75 | rdoc (~> 3.4) 76 | thor (>= 0.14.6, < 2.0) 77 | rake (10.4.2) 78 | rdoc (3.12.2) 79 | json (~> 1.4) 80 | rspec (2.99.0) 81 | rspec-core (~> 2.99.0) 82 | rspec-expectations (~> 2.99.0) 83 | rspec-mocks (~> 2.99.0) 84 | rspec-core (2.99.2) 85 | rspec-expectations (2.99.2) 86 | diff-lcs (>= 1.1.3, < 2.0) 87 | rspec-mocks (2.99.3) 88 | sprockets (2.2.3) 89 | hike (~> 1.2) 90 | multi_json (~> 1.0) 91 | rack (~> 1.0) 92 | tilt (~> 1.1, != 1.3.0) 93 | thor (0.19.1) 94 | tilt (1.4.1) 95 | treetop (1.4.15) 96 | polyglot 97 | polyglot (>= 0.3.1) 98 | tzinfo (0.3.44) 99 | 100 | PLATFORMS 101 | ruby 102 | 103 | DEPENDENCIES 104 | appraisal 105 | bundler (>= 1.3.5) 106 | google_visualr! 107 | rails (~> 3.2.21) 108 | rspec (~> 2.99.0) 109 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Change Log 2 | 3 | ### Version 2.5.1 4 | 5 | * [Pull Request 98](https://github.com/winston/google_visualr/pull/97) Do not force `en` as the default language. 6 | 7 | ### Version 2.5.0 8 | 9 | * [Pull Request 97](https://github.com/winston/google_visualr/pull/97) Add new Google charts. 10 | * [Pull Request 96](https://github.com/winston/google_visualr/pull/96) Allow charts to have a different locale. 11 | * [Pull Request 95](https://github.com/winston/google_visualr/pull/95) Allow charts to be styled with Material Design. 12 | * [Pull Request 72](https://github.com/winston/google_visualr/pull/72) Add ability to specify version of Google Chart library. 13 | 14 | ### Version 2.4.0 15 | 16 | * [Pull Request 75](https://github.com/winston/google_visualr/pull/75) Add Histogram chart. 17 | 18 | ### Version 2.3.0 19 | 20 | * [Issue 69](https://github.com/winston/google_visualr/pull/69) Support generating chart Javascript without `" 69 | js 70 | end 71 | 72 | # Generates JavaScript for loading the appropriate Google Visualization package, with callback to render chart. 73 | # 74 | # Parameters: 75 | # *div_id [Required] The ID of the DIV element that the Google Chart should be rendered in. 76 | def load_js(element_id) 77 | language_opt = ", language: '#{language}'" if language 78 | 79 | "\n google.load('visualization', '#{version}', {packages: ['#{package_name}']#{language_opt}, callback: #{chart_function_name(element_id)}});" 80 | end 81 | 82 | # Generates JavaScript function for rendering the chart. 83 | # 84 | # Parameters: 85 | # *div_id [Required] The ID of the DIV element that the Google Chart should be rendered in. 86 | def draw_js(element_id) 87 | js = "" 88 | js << "\n function #{chart_function_name(element_id)}() {" 89 | js << "\n #{@data_table.to_js}" 90 | js << "\n var chart = new google.#{chart_class}.#{chart_name}(document.getElementById('#{element_id}'));" 91 | @listeners.each do |listener| 92 | js << "\n google.visualization.events.addListener(chart, '#{listener[:event]}', #{listener[:callback]});" 93 | end 94 | js << "\n chart.draw(data_table, #{js_parameters(@options)});" 95 | js << "\n };" 96 | js 97 | end 98 | end 99 | 100 | end 101 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both thread web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | if Rails.version >= "4.2.0" 24 | config.serve_static_files = false 25 | else 26 | config.serve_static_assets = false 27 | end 28 | 29 | # Compress JavaScripts and CSS. 30 | config.assets.js_compressor = :uglifier 31 | # config.assets.css_compressor = :sass 32 | 33 | # Whether to fallback to assets pipeline if a precompiled asset is missed. 34 | config.assets.compile = false 35 | 36 | # Generate digests for assets URLs. 37 | config.assets.digest = true 38 | 39 | # Version of your assets, change this if you want to expire all your assets. 40 | config.assets.version = '1.0' 41 | 42 | # Specifies the header that your server uses for sending files. 43 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 44 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 45 | 46 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 47 | # config.force_ssl = true 48 | 49 | # Set to :debug to see everything in the log. 50 | config.log_level = :info 51 | 52 | # Prepend all log lines with the following tags. 53 | # config.log_tags = [:subdomain, :uuid] 54 | 55 | # Use a different logger for distributed setups. 56 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 57 | 58 | # Use a different cache store in production. 59 | # config.cache_store = :mem_cache_store 60 | 61 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 62 | # config.action_controller.asset_host = "http://assets.example.com" 63 | 64 | # Precompile additional assets. 65 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 66 | # config.assets.precompile += %w( search.js ) 67 | 68 | # Ignore bad email addresses and do not raise email delivery errors. 69 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 70 | # config.action_mailer.raise_delivery_errors = false 71 | 72 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 73 | # the I18n.default_locale when a translation can not be found). 74 | config.i18n.fallbacks = true 75 | 76 | # Send deprecation notices to registered listeners. 77 | config.active_support.deprecation = :notify 78 | 79 | # Disable automatic flushing of the log to improve performance. 80 | # config.autoflush_log = false 81 | 82 | # Use default logging formatter so that PID and timestamp are not suppressed. 83 | config.log_formatter = ::Logger::Formatter.new 84 | end 85 | -------------------------------------------------------------------------------- /lib/google_visualr/packages.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | module Packages 3 | module CoreChart 4 | def package_name 5 | "corechart" 6 | end 7 | end 8 | 9 | module ImageChart 10 | def package_name 11 | "image#{self.class.to_s.split("::").last.downcase}" 12 | end 13 | def class_name 14 | "Image#{self.class.to_s.split('::').last}" 15 | end 16 | 17 | # Set defaults according to http://code.google.com/apis/chart/interactive/docs/gallery/genericimagechart.html#Configuration_Options 18 | IMAGE_DEFAULTS = { 19 | # Automatic Scaling 20 | :chds => "a", 21 | # Size 22 | :chs => "400x200", 23 | # Axes 24 | :chxt => "x,y" 25 | } 26 | 27 | # Generates HTTP GET URL for the chart image 28 | # 29 | # Parameters: 30 | # *opts [Optional] Hash of standard chart options (see http://code.google.com/apis/chart/image/docs/chart_params.html) 31 | def chart_image_url(superseding_params = {}) 32 | 33 | ##### 34 | # Generic image chart defaults 35 | query_params = IMAGE_DEFAULTS.clone 36 | 37 | # backgroundColor 38 | query_params[:chf] = "bg,s," + options["backgroundColor"].gsub(/#/, '') if options["backgroundColor"] 39 | 40 | # color, colors ('color' param is ignored if 'colors' is present) 41 | if options["colors"] 42 | query_params[:chco] = options["colors"].join(',').gsub(/#/, '') 43 | elsif options["color"] 44 | query_params[:chco] = options["color"].gsub(/#/, '') 45 | end 46 | 47 | # fill (this will often not look good - better for user to override this parameter) 48 | query_params[:chm] = "B,#{query_params[:chco].split(',').first},0,0,0" if options["fill"] && query_params[:chco] 49 | 50 | # firstHiddenColumn, singleColumnDisplay, data 51 | firstHiddenColumn = options["firstHiddenColumn"] ? options["firstHiddenColumn"] : data_table.cols.size - 1 52 | query_params[:chd] = "t:" 53 | unless options["singleColumnDisplay"] 54 | for i in 1..firstHiddenColumn do 55 | query_params[:chd] += "|" if i > 1 56 | query_params[:chd] += data_table.get_column(i).join(',') 57 | end 58 | else 59 | query_params[:chd] += data_dable.get_column(options["singleColumnDisplay"]) 60 | end 61 | 62 | # height, width 63 | if options["height"] && options["width"] 64 | query_params[:chs] = "#{options["width"]}x#{options["height"]}" 65 | end 66 | 67 | # title 68 | query_params[:chtt] = options["title"] if options["title"] 69 | 70 | # legend 71 | unless options["legend"] == 'none' 72 | query_params[:chdlp] = options["legend"].first unless options["legend"].blank? 73 | query_params[:chdl] = data_table.cols[1..-1].map{|col| col[:label] }.join('|') 74 | else 75 | query_params.delete(:chdlp) 76 | query_params.delete(:chdl) 77 | end 78 | 79 | # min, max, valueLabelsInterval (works as long as :chxt => "x,y" and both 'min' and 'max' are set) 80 | if options["min"] && options["max"] 81 | query_params[:chxr] = "1,#{options['min']},#{options['max']}" 82 | query_params[:chxr] += ",#{options['valueLabelsInterval']}" if options['valueLabelsInterval'] 83 | query_params[:chds] = "#{options['min']},#{options['max']}" 84 | end 85 | ##### 86 | 87 | query_params = stringify_keys!(query_params.merge(superseding_params)) 88 | base_url = "https://chart.googleapis.com/chart" 89 | query = "" 90 | query_params.each_with_index do |(k,v),i| 91 | query += (i == 0) ? "?" : "&" 92 | query += "#{k}=#{CGI.escape(v)}" 93 | end 94 | URI.parse(base_url + query) 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/support/common.rb: -------------------------------------------------------------------------------- 1 | def data_table 2 | @cols = [ 3 | { :type => "string", :label => "Year" }, 4 | { :type => "number", :label => "Sales" }, 5 | { :type => "number", :label => "Expenses" } 6 | ] 7 | @rows = [ 8 | { :c => [{ :v => "2004" }, { :v => 1000 }, { :v => 400 }] }, 9 | { :c => [{ :v => "2005" }, { :v => 1200 }, { :v => 450 }] }, 10 | { :c => [{ :v => "2006" }, { :v => 1500 }, { :v => 600 }] }, 11 | { :c => [{ :v => "2007" }, { :v => 800 }, { :v => 500 }] } 12 | ] 13 | GoogleVisualr::DataTable.new(:cols => @cols, :rows => @rows) 14 | end 15 | 16 | def base_chart(data_table = data_table()) 17 | GoogleVisualr::BaseChart.new(data_table, { :legend => "Test Chart", :width => 800, :is3D => true }) 18 | end 19 | 20 | def base_chart_js_without_script_tag(div_class = "div_class", language = nil) 21 | language_opt = ", language: '#{language}'" if language 22 | 23 | js = "\n google.load('visualization', '1.0', {packages: ['basechart']#{language_opt}, callback: draw_#{div_class}});" 24 | js << "\n function draw_#{div_class}() {" 25 | js << "\n var data_table = new google.visualization.DataTable();data_table.addColumn({\"type\":\"string\",\"label\":\"Year\"});data_table.addColumn({\"type\":\"number\",\"label\":\"Sales\"});data_table.addColumn({\"type\":\"number\",\"label\":\"Expenses\"});data_table.addRow([{v: \"2004\"}, {v: 1000}, {v: 400}]);data_table.addRow([{v: \"2005\"}, {v: 1200}, {v: 450}]);data_table.addRow([{v: \"2006\"}, {v: 1500}, {v: 600}]);data_table.addRow([{v: \"2007\"}, {v: 800}, {v: 500}]);\n var chart = new google.visualization.BaseChart(document.getElementById('#{div_class}'));" 26 | js << "\n chart.draw(data_table, {legend: \"Test Chart\", width: 800, is3D: true});" 27 | js << "\n };" 28 | end 29 | 30 | def base_chart_js(div_class = "div_class", language = nil) 31 | js = "\n" 34 | end 35 | 36 | def base_chart_with_listener_js(div_class = "div_class") 37 | js = "\n" 45 | end 46 | 47 | def material_chart(div_class = "div_class") 48 | js = "\n" 55 | end 56 | -------------------------------------------------------------------------------- /spec/google_visualr/base_chart_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GoogleVisualr::BaseChart do 4 | 5 | before do 6 | @dt = data_table 7 | @chart = base_chart(@dt) 8 | end 9 | 10 | describe "#initialize" do 11 | it "works" do 12 | @chart.data_table.should == @dt 13 | @chart.version.should == GoogleVisualr::BaseChart::DEFAULT_VERSION 14 | @chart.material.should == false 15 | @chart.options.should == { "legend" => "Test Chart", "width" => 800, "is3D" => true } 16 | @chart.language.should == nil 17 | end 18 | 19 | it "accepts version attribute" do 20 | @chart = GoogleVisualr::BaseChart.new(@dt, version: "1.1") 21 | @chart.version.should == "1.1" 22 | end 23 | 24 | it "accepts language attribute" do 25 | @chart = GoogleVisualr::BaseChart.new(@dt, language: "ja") 26 | @chart.language.should == "ja" 27 | end 28 | 29 | it "accepts material attribute" do 30 | @chart = GoogleVisualr::BaseChart.new(@dt, material: true) 31 | @chart.material.should == true 32 | end 33 | end 34 | 35 | describe "#package_name" do 36 | it "returns class name (without module) and downcased" do 37 | @chart.package_name.should == "basechart" 38 | end 39 | end 40 | 41 | describe "#class_name" do 42 | it "returns class name (without module)" do 43 | @chart.class_name.should == "BaseChart" 44 | end 45 | end 46 | 47 | describe "#chart_class" do 48 | it "returns 'charts' when material is true" do 49 | @chart.material = true 50 | @chart.chart_class.should == "charts" 51 | end 52 | 53 | it "returns 'charts' when material is false" do 54 | @chart.material = false 55 | @chart.chart_class.should == "visualization" 56 | end 57 | end 58 | 59 | describe "#chart_name" do 60 | it "returns class name (less module) when material is true" do 61 | @chart.material = true 62 | @chart.chart_name.should == "Base" 63 | end 64 | 65 | it "returns class name (less module) when material is false" do 66 | @chart.material = false 67 | @chart.chart_name.should == "BaseChart" 68 | end 69 | end 70 | 71 | describe "#chart_function_name" do 72 | it "returns a function name that is used to draw the chart in JS" do 73 | @chart.chart_function_name("base_chart").should == "draw_base_chart" 74 | end 75 | 76 | it "handles 'dashes' in element id" do 77 | @chart.chart_function_name("base-chart").should == "draw_base_chart" 78 | end 79 | end 80 | 81 | describe "#options=" do 82 | it "works" do 83 | @chart.options = { :legend => "Awesome Chart", :width => 1000, :is3D => false } 84 | @chart.options.should == { "legend" => "Awesome Chart", "width" => 1000, "is3D" => false } 85 | end 86 | end 87 | 88 | describe "#add_listener" do 89 | it "adds to listeners array" do 90 | @chart.add_listener("select", "function() {test_event(chart);}") 91 | @chart.listeners.should == [{ :event => "select", :callback => "function() {test_event(chart);}" }] 92 | end 93 | end 94 | 95 | describe "#to_js" do 96 | it "generates JS" do 97 | js = @chart.to_js("body") 98 | js.should == base_chart_js("body") 99 | js.should include(" "150px" } ) 16 | formatter.instance_variable_get(:@options).should == { :width => "150px" } 17 | end 18 | end 19 | 20 | describe "#columns" do 21 | it "sets columns" do 22 | formatter = valid_object 23 | formatter.columns(0) 24 | formatter.instance_variable_get(:@columns).should == [0] 25 | formatter.columns(1,2) 26 | formatter.instance_variable_get(:@columns).should == [1,2] 27 | end 28 | end 29 | 30 | describe "#options" do 31 | it "sets options" do 32 | formatter = valid_object 33 | formatter.options( :width => "150px", :height => "150px" ) 34 | formatter.instance_variable_get(:@options).should == { "width" => "150px", "height" => "150px" } 35 | end 36 | end 37 | 38 | context "GoogleVisualr::ArrowFormat" do 39 | describe "#to_js" do 40 | it "works" do 41 | formatter = GoogleVisualr::ArrowFormat.new(:base => 100) 42 | formatter.columns(1) 43 | formatter.to_js.should == "\nvar formatter = new google.visualization.ArrowFormat({base: 100});\nformatter.format(data_table, 1);" 44 | end 45 | end 46 | end 47 | 48 | context "GoogleVisualr::BarFormat" do 49 | describe "#to_js" do 50 | it "works" do 51 | formatter = GoogleVisualr::BarFormat.new(:base => 100, :colorNegative => 'red', :colorPositive => 'green', :drawZeroLine => false, :max => 1000, :min => -1000, :showValue => false, :width => '150px') 52 | formatter.columns(1) 53 | formatter.to_js.should == "\nvar formatter = new google.visualization.BarFormat({base: 100, colorNegative: \"red\", colorPositive: \"green\", drawZeroLine: false, max: 1000, min: -1000, showValue: false, width: \"150px\"});\nformatter.format(data_table, 1);" 54 | end 55 | end 56 | end 57 | 58 | context "GoogleVisualr::ColorFormat" do 59 | describe "#add_range" do 60 | it "sets color range" do 61 | formatter = GoogleVisualr::ColorFormat.new 62 | formatter.add_range(20000, nil, 'red', '#333333') 63 | formatter.ranges.should == [ {:from => 20000, :to => nil , :color => "red", :bgcolor => "#333333"} ] 64 | end 65 | end 66 | 67 | describe "#add_gradient_range" do 68 | it "sets gradient color range" do 69 | formatter = GoogleVisualr::ColorFormat.new 70 | formatter.add_gradient_range(20000, nil, 'red', '#FFFFFF', '#333333') 71 | formatter.gradient_ranges.should == [ {:from => 20000, :to => nil , :color => "red", :fromBgColor => "#FFFFFF", :toBgColor => "#333333"} ] 72 | end 73 | end 74 | 75 | describe "#to_js" do 76 | it "works" do 77 | formatter = GoogleVisualr::ColorFormat.new 78 | formatter.add_range(0, 1000, 'red', '#000000') 79 | formatter.add_gradient_range(2000, nil, 'blue', '#FFFFFF', '#333333') 80 | formatter.columns(1) 81 | formatter.to_js.should == "\nvar formatter = new google.visualization.ColorFormat();\nformatter.addRange(0, 1000, \"red\", \"#000000\");\nformatter.addGradientRange(2000, null, \"blue\", \"#FFFFFF\", \"#333333\");\nformatter.format(data_table, 1);" 82 | end 83 | end 84 | end 85 | 86 | context "GoogleVisualr::DateFormat" do 87 | describe "#to_js" do 88 | it "works" do 89 | formatter = GoogleVisualr::DateFormat.new(:formatType => 'long', :timeZone => 8) 90 | formatter.columns(1) 91 | formatter.to_js.should == "\nvar formatter = new google.visualization.DateFormat({formatType: \"long\", timeZone: 8});\nformatter.format(data_table, 1);" 92 | end 93 | end 94 | end 95 | 96 | context "GoogleVisualr::NumberFormat" do 97 | describe "#to_js" do 98 | it "works" do 99 | formatter = GoogleVisualr::NumberFormat.new(:decimalSymbol => '.', :fractionDigits => 4, :groupingSymbol => ',', :negativeColor => 'red', :negativeParens => false, :prefix => 'USD$', :suffix => '-') 100 | formatter.columns(1) 101 | formatter.to_js.should == "\nvar formatter = new google.visualization.NumberFormat({decimalSymbol: \".\", fractionDigits: 4, groupingSymbol: \",\", negativeColor: \"red\", negativeParens: false, prefix: \"USD$\", suffix: \"-\"});\nformatter.format(data_table, 1);" 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/dummy/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ruby on Rails: Welcome aboard 5 | 172 | 185 | 186 | 187 |
188 | 201 | 202 |
203 | 207 | 208 | 212 | 213 |
214 |

Getting started

215 |

Here’s how to get rolling:

216 | 217 |
    218 |
  1. 219 |

    Use rails generate to create your models and controllers

    220 |

    To see all available options, run it without parameters.

    221 |
  2. 222 | 223 |
  3. 224 |

    Set up a default route and remove or rename this file

    225 |

    Routes are set up in config/routes.rb.

    226 |
  4. 227 | 228 |
  5. 229 |

    Create your database

    230 |

    Run rake db:migrate to create your database. If you're not using SQLite (the default), edit config/database.yml with your username and password.

    231 |
  6. 232 |
233 |
234 |
235 | 236 | 237 |
238 | 239 | 240 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoogleVisualr 2 | 3 | [![Gem Version](http://img.shields.io/gem/v/google_visualr.svg?style=flat-square)](https://rubygems.org/gems/google_visualr) 4 | [![Build Status](http://img.shields.io/travis/winston/google_visualr.svg?style=flat-square)](https://travis-ci.org/winston/google_visualr) 5 | [![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](https://github.com/winston/google_visualr/blob/master/MIT-LICENSE) 6 | 7 | GoogleVisualr, is a wrapper around the [Google Charts](https://developers.google.com/chart/) that allows anyone to create beautiful charts with just plain Ruby. You don't have to write any JavaScript at all. 8 | 9 | It's good for any Ruby (Rails/Sinatra) setup, and you can handle the entire charting logic in Ruby. 10 | 11 | Please refer to the [GoogleVisualr API Reference site](http://googlevisualr.herokuapp.com/) for a complete list of Google charts that you can create with GoogleVisualr. 12 | 13 | ## tl:dr 14 | 15 | * In your model or controller, write Ruby code to create your chart (e.g. Area Chart, Bar Chart etc). 16 | * Configure your chart with any of the options as listed in Google's API Docs. E.g. [Area Chart](http://code.google.com/apis/chart/interactive/docs/gallery/areachart.html#Configuration_Options). 17 | * In your view, call `chart.to_js(div_id)` and that renders JavaScript into the HTML output. 18 | * You get your awesome Google chart, and you didn't write any JavaScript! 19 | 20 | ## Limitations 21 | 22 | GoogleVisualr is not a 100% complete wrapper for Google Chart Tools. 23 | 24 | `Methods` and `Events` as described in Google's API Docs - for use after a chart has been rendered, 25 | are not implemented because they feel more natural being written as JavaScript functions, within the views or .js files. 26 | 27 | ## Install 28 | 29 | Assuming you are on Rails 3/4, include the gem in your Gemfile. 30 | 31 | gem "google_visualr", ">= 2.5" 32 | 33 | ## Basics 34 | 35 | This is a basic implementation of `GoogleVisualr::DataTable` and `GoogleVisualr::AreaChart`. 36 | 37 | For detailed documentation, please refer to the [GoogleVisualr API Reference](http://googlevisualr.herokuapp.com/) or [read the source](https://github.com/winston/google_visualr_app). 38 | 39 | --- 40 | 41 | In your Rails layout, load Google Ajax API in the head tag, at the very top. 42 | 43 | 44 | 45 | In your Rails controller, initialize a GoogleVisualr::DataTable object with an empty constructor. 46 | 47 | data_table = GoogleVisualr::DataTable.new 48 | 49 | Populate data_table with column headers, and row values. 50 | 51 | # Add Column Headers 52 | data_table.new_column('string', 'Year' ) 53 | data_table.new_column('number', 'Sales') 54 | data_table.new_column('number', 'Expenses') 55 | 56 | # Add Rows and Values 57 | data_table.add_rows([ 58 | ['2004', 1000, 400], 59 | ['2005', 1170, 460], 60 | ['2006', 660, 1120], 61 | ['2007', 1030, 540] 62 | ]) 63 | 64 | Create a GoogleVisualr::AreaChart with data_table and configuration options. 65 | 66 | option = { width: 400, height: 240, title: 'Company Performance' } 67 | @chart = GoogleVisualr::Interactive::AreaChart.new(data_table, option) 68 | 69 | In your Rails view, render the Google chart. 70 | 71 |
72 | <%= render_chart(@chart, 'chart') %> 73 | 74 | ## Chart Initializer 75 | 76 | The initializer of `GoogleVisualr::` takes in two parameters: `data_table` and `options`. 77 | 78 | ### `data_table` 79 | 80 | `data_table` is an instance of `GoogleVisualr::DataTable` and contains headers and rows of data. 81 | 82 | ### `options` 83 | 84 | `options` is a hash of configuration options for the Google chart (e.g. width, height, colors etc). 85 | 86 | The available configuration options are exactly the same as those specified in Google's API Docs. 87 | 88 | For example: 89 | - [Configuration options for AreaChart](https://developers.google.com/chart/interactive/docs/gallery/areachart#configuration-options) 90 | - [Configuration options for BarChart](https://developers.google.com/chart/interactive/docs/gallery/barchart#configuration-options) 91 | 92 | At the same time, you can also specify `version`, `language` and `material` as configuration options. 93 | 94 | #### `version` 95 | 96 | The default version of Google Charts library loaded is `1.0`. 97 | 98 | However, some charts (e.g. Gantt Charts) require the latest version of the Google Charts library, 99 | so you will have to specify `version` as `1.1` in the configuration options 100 | 101 | ``` 102 | .... 103 | @chart = GoogleVisualr::GanttChart.new(data_table, { version: '1.1' }) 104 | ``` 105 | 106 | [Read more about it on Google's API Docs](https://developers.google.com/chart/interactive/docs/basic_load_libs). 107 | 108 | #### `language` 109 | 110 | The default locale of all Google Charts is `en`. 111 | 112 | You can override this default by specifying `language` in the configuration options. 113 | 114 | ``` 115 | .... 116 | @chart = GoogleVisualr::BarChart.new(data_table, { language: 'ja' }) 117 | ``` 118 | 119 | [Read more about it on Google's API Docs](https://developers.google.com/chart/interactive/docs/library_loading_enhancements#loadwithlocale). 120 | 121 | #### `material` 122 | 123 | Google has also enabled `Material` design for some charts: 124 | 125 | > In 2014, Google announced guidelines intended to support a common look and feel across its properties and apps (such as Android apps) that run on Google platforms. We call this effort Material Design. We'll be providing "Material" versions of all our core charts; you're welcome to use them if you like how they look. 126 | 127 | To use material design, you will have to specify `material` as `true` 128 | in the configuration options. 129 | 130 | ``` 131 | .... 132 | @chart = GoogleVisualr::BarChart.new(data_table, { material: true }) 133 | ``` 134 | 135 | ## Listeners 136 | 137 | For an example usage of `listeners`, please refer to [this comment](https://github.com/winston/google_visualr/issues/36#issuecomment-9880256). 138 | 139 | Besides `listeners` you can now also redraw charts in your JavaScript maunally by calling `draw_()` function. See [this commit](https://github.com/winston/google_visualr/commit/e5554886bd83f56dd31bbc543fdcf1e24523776a) for more details. 140 | 141 | ## Support 142 | 143 | Please submit all feedback, bugs and feature-requests to [GitHub Issues Tracker](http://github.com/winston/google_visualr/issues). 144 | 145 | Feel free to fork the project, make improvements or bug fixes and submit pull requests (with tests!). 146 | 147 | ## Who's Using GoogleVisualr? 148 | 149 | I would like to collect some data about who's using this Gem. [Please drop me a line](mailto:winstonyw+googlevisualr@gmail.com). 150 | 151 | ## Author 152 | 153 | GoogleVisualr is maintained by [Winston Teo](mailto:winstonyw+googlevisualr@gmail.com). 154 | 155 | Who is Winston Teo? [You should follow Winston on Twitter](http://www.twitter.com/winstonyw), or find out more on [WinstonYW](http://www.winstonyw.com). 156 | 157 | ## License 158 | 159 | Copyright © 2015 Winston Teo Yong Wei. Free software, released under the MIT license. 160 | -------------------------------------------------------------------------------- /spec/dummy/public/javascripts/rails.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // Technique from Juriy Zaytsev 3 | // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ 4 | function isEventSupported(eventName) { 5 | var el = document.createElement('div'); 6 | eventName = 'on' + eventName; 7 | var isSupported = (eventName in el); 8 | if (!isSupported) { 9 | el.setAttribute(eventName, 'return;'); 10 | isSupported = typeof el[eventName] == 'function'; 11 | } 12 | el = null; 13 | return isSupported; 14 | } 15 | 16 | function isForm(element) { 17 | return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM' 18 | } 19 | 20 | function isInput(element) { 21 | if (Object.isElement(element)) { 22 | var name = element.nodeName.toUpperCase() 23 | return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA' 24 | } 25 | else return false 26 | } 27 | 28 | var submitBubbles = isEventSupported('submit'), 29 | changeBubbles = isEventSupported('change') 30 | 31 | if (!submitBubbles || !changeBubbles) { 32 | // augment the Event.Handler class to observe custom events when needed 33 | Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap( 34 | function(init, element, eventName, selector, callback) { 35 | init(element, eventName, selector, callback) 36 | // is the handler being attached to an element that doesn't support this event? 37 | if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) || 38 | (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) { 39 | // "submit" => "emulated:submit" 40 | this.eventName = 'emulated:' + this.eventName 41 | } 42 | } 43 | ) 44 | } 45 | 46 | if (!submitBubbles) { 47 | // discover forms on the page by observing focus events which always bubble 48 | document.on('focusin', 'form', function(focusEvent, form) { 49 | // special handler for the real "submit" event (one-time operation) 50 | if (!form.retrieve('emulated:submit')) { 51 | form.on('submit', function(submitEvent) { 52 | var emulated = form.fire('emulated:submit', submitEvent, true) 53 | // if custom event received preventDefault, cancel the real one too 54 | if (emulated.returnValue === false) submitEvent.preventDefault() 55 | }) 56 | form.store('emulated:submit', true) 57 | } 58 | }) 59 | } 60 | 61 | if (!changeBubbles) { 62 | // discover form inputs on the page 63 | document.on('focusin', 'input, select, texarea', function(focusEvent, input) { 64 | // special handler for real "change" events 65 | if (!input.retrieve('emulated:change')) { 66 | input.on('change', function(changeEvent) { 67 | input.fire('emulated:change', changeEvent, true) 68 | }) 69 | input.store('emulated:change', true) 70 | } 71 | }) 72 | } 73 | 74 | function handleRemote(element) { 75 | var method, url, params; 76 | 77 | var event = element.fire("ajax:before"); 78 | if (event.stopped) return false; 79 | 80 | if (element.tagName.toLowerCase() === 'form') { 81 | method = element.readAttribute('method') || 'post'; 82 | url = element.readAttribute('action'); 83 | params = element.serialize(); 84 | } else { 85 | method = element.readAttribute('data-method') || 'get'; 86 | url = element.readAttribute('href'); 87 | params = {}; 88 | } 89 | 90 | new Ajax.Request(url, { 91 | method: method, 92 | parameters: params, 93 | evalScripts: true, 94 | 95 | onComplete: function(request) { element.fire("ajax:complete", request); }, 96 | onSuccess: function(request) { element.fire("ajax:success", request); }, 97 | onFailure: function(request) { element.fire("ajax:failure", request); } 98 | }); 99 | 100 | element.fire("ajax:after"); 101 | } 102 | 103 | function handleMethod(element) { 104 | var method = element.readAttribute('data-method'), 105 | url = element.readAttribute('href'), 106 | csrf_param = $$('meta[name=csrf-param]')[0], 107 | csrf_token = $$('meta[name=csrf-token]')[0]; 108 | 109 | var form = new Element('form', { method: "POST", action: url, style: "display: none;" }); 110 | element.parentNode.insert(form); 111 | 112 | if (method !== 'post') { 113 | var field = new Element('input', { type: 'hidden', name: '_method', value: method }); 114 | form.insert(field); 115 | } 116 | 117 | if (csrf_param) { 118 | var param = csrf_param.readAttribute('content'), 119 | token = csrf_token.readAttribute('content'), 120 | field = new Element('input', { type: 'hidden', name: param, value: token }); 121 | form.insert(field); 122 | } 123 | 124 | form.submit(); 125 | } 126 | 127 | 128 | document.on("click", "*[data-confirm]", function(event, element) { 129 | var message = element.readAttribute('data-confirm'); 130 | if (!confirm(message)) event.stop(); 131 | }); 132 | 133 | document.on("click", "a[data-remote]", function(event, element) { 134 | if (event.stopped) return; 135 | handleRemote(element); 136 | event.stop(); 137 | }); 138 | 139 | document.on("click", "a[data-method]", function(event, element) { 140 | if (event.stopped) return; 141 | handleMethod(element); 142 | event.stop(); 143 | }); 144 | 145 | document.on("submit", function(event) { 146 | var element = event.findElement(), 147 | message = element.readAttribute('data-confirm'); 148 | if (message && !confirm(message)) { 149 | event.stop(); 150 | return false; 151 | } 152 | 153 | var inputs = element.select("input[type=submit][data-disable-with]"); 154 | inputs.each(function(input) { 155 | input.disabled = true; 156 | input.writeAttribute('data-original-value', input.value); 157 | input.value = input.readAttribute('data-disable-with'); 158 | }); 159 | 160 | var element = event.findElement("form[data-remote]"); 161 | if (element) { 162 | handleRemote(element); 163 | event.stop(); 164 | } 165 | }); 166 | 167 | document.on("ajax:after", "form", function(event, element) { 168 | var inputs = element.select("input[type=submit][disabled=true][data-disable-with]"); 169 | inputs.each(function(input) { 170 | input.value = input.readAttribute('data-original-value'); 171 | input.removeAttribute('data-original-value'); 172 | input.disabled = false; 173 | }); 174 | }); 175 | 176 | Ajax.Responders.register({ 177 | onCreate: function(request) { 178 | var csrf_meta_tag = $$('meta[name=csrf-token]')[0]; 179 | 180 | if (csrf_meta_tag) { 181 | var header = 'X-CSRF-Token', 182 | token = csrf_meta_tag.readAttribute('content'); 183 | 184 | if (!request.options.requestHeaders) { 185 | request.options.requestHeaders = {}; 186 | } 187 | request.options.requestHeaders[header] = token; 188 | } 189 | } 190 | }); 191 | })(); 192 | -------------------------------------------------------------------------------- /spec/google_visualr/image_charts_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Warning.. 4 | # Image Charts are deprecated as iof 20 Apr 2012, but supported till 2015. 5 | # We might remove these soon 6 | 7 | shared_examples_for "image chart" do 8 | it "generates correct URI" do 9 | CGI.parse(chart.uri.query).should == CGI.parse(uri.query) 10 | end 11 | end 12 | 13 | describe GoogleVisualr::Image::PieChart do 14 | let(:chart) { GoogleVisualr::Image::PieChart.new( data_table, options ) } 15 | 16 | it_should_behave_like "image chart" do 17 | let(:options) { { :title => "Favorite Desserts", :is3D => false, :labels => "none" } } 18 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chdl=2004%7C2005%7C2006%7C2007&chs=400x200&cht=p&chtt=Favorite+Desserts&chl=&chd=t%3A1000%2C1200%2C1500%2C800&chds=a") } 19 | end 20 | 21 | it_should_behave_like "image chart" do 22 | let(:options) { { :title => "Favorite Desserts", :is3D => false, :labels => "name", :backgroundColor => "#EFEFEF", :legend => "left" } } 23 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chf=bg%2Cs%2CEFEFEF&chdl=2004%7C2005%7C2006%7C2007&chs=400x200&cht=p&chdlp=l&chtt=Favorite+Desserts&chl=2004%7C2005%7C2006%7C2007&chd=t%3A1000%2C1200%2C1500%2C800&chds=a") } 24 | end 25 | 26 | it_should_behave_like "image chart" do 27 | let(:options) { { :title => "Favorite Desserts", :width => 650, :height => 300, :is3D => true, :labels => "value", :backgroundColor => "#FFFFFF", :legend => "bottom" } } 28 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chf=bg%2Cs%2CFFFFFF&chdl=2004%7C2005%7C2006%7C2007&chs=650x300&cht=p3&chdlp=b&chtt=Favorite+Desserts&chl=1000%7C1200%7C1500%7C800&chd=t%3A1000%2C1200%2C1500%2C800&chds=a") } 29 | end 30 | 31 | it_should_behave_like "image chart" do 32 | let(:options) { { :title => "Favorite Desserts", :width => 650, :height => 300, :is3D => false, :labels => "none", :backgroundColor => "#FFFFFF", :color => "#444D92", :legend => "top" } } 33 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chf=bg%2Cs%2CFFFFFF&chdl=2004%7C2005%7C2006%7C2007&chs=650x300&cht=p&chdlp=t&chtt=Favorite+Desserts&chco=444D92&chl=&chd=t%3A1000%2C1200%2C1500%2C800&chds=a") } 34 | end 35 | 36 | it_should_behave_like "image chart" do 37 | let(:options) { { :title => "Favorite Desserts", :width => 650, :height => 300, :is3D => true, :labels => "none", :backgroundColor => "#FFFFFF", :colors => %w(#444D92 #4c56a2 #5E67AB #7078B5 #8289BE #949AC7 #A6AAD0 #B7BBDA #C9CCE3 #DBDDEC #EDEEF6), :legend => "none" } } 38 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chf=bg%2Cs%2CFFFFFF&chdl=2004%7C2005%7C2006%7C2007&chs=650x300&cht=p3&chtt=Favorite+Desserts&chco=444D92%7C4c56a2%7C5E67AB%7C7078B5%7C8289BE%7C949AC7%7CA6AAD0%7CB7BBDA%7CC9CCE3%7CDBDDEC%7CEDEEF6&chl=&chd=t%3A1000%2C1200%2C1500%2C800&chds=a") } 39 | end 40 | end 41 | 42 | describe GoogleVisualr::Image::LineChart do 43 | let(:chart) { GoogleVisualr::Image::LineChart.new( data_table, options ) } 44 | 45 | it_should_behave_like "image chart" do 46 | let(:options) { { :title => "Test1 Line Chart", :colors => ['#437E9D', '#E6A65A'], :legend => "left", :backgroundColor => "#EFEFEF", :showCategoryLabels => true, :showValueLabels => false } } 47 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chf=bg%2Cs%2CEFEFEF&chdl=Sales%7CExpenses&chs=400x200&chxl=0%3A%7C2004%7C2005%7C2006%7C2007%7C1%3A%7C%7C&cht=lc&chdlp=l&chtt=Test1+Line+Chart&chco=437E9D%2CE6A65A&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500&chds=a") } 48 | end 49 | 50 | it_should_behave_like "image chart" do 51 | let(:options) { { :title => "Test2 Line Chart", :width => 500, :height => 200, :color => '#437E9D', :legend => "right", :backgroundColor => "#FFFFFF", :showCategoryLabels => false, :showValueLabels => true } } 52 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chf=bg%2Cs%2CFFFFFF&chdl=Sales%7CExpenses&chs=500x200&chxl=0%3A%7C%7C&cht=lc&chdlp=r&chtt=Test2+Line+Chart&chco=437E9D&chds=a&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500") } 53 | end 54 | 55 | it_should_behave_like "image chart" do 56 | let(:options) { { :title => "Test3 Line Chart", :width => 500, :height => 200, :colors => ['#437E9D', '#E6A65A'], :legend => "top", :showCategoryLabels => false, :showValueLabels => false, :showAxisLines => false } } 57 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chdl=Sales%7CExpenses&chs=500x200&chxl=0%3A%7C%7C1%3A%7C%7C&cht=lc%3Anda&chdlp=t&chtt=Test3+Line+Chart&chco=437E9D%2CE6A65A&chds=a&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500") } 58 | end 59 | 60 | it_should_behave_like "image chart" do 61 | let(:options) { { :title => "Test4 Line Chart", :width => 500, :height => 200, :color => '#437E9D', :legend => "bottom", :showCategoryLabels => true, :showValueLabels => true, :showAxisLines => true } } 62 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chdl=Sales%7CExpenses&chs=500x200&chxl=0%3A%7C2004%7C2005%7C2006%7C2007%7C&cht=lc&chdlp=b&chtt=Test4+Line+Chart&chco=437E9D&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500&chds=a") } 63 | end 64 | 65 | it_should_behave_like "image chart" do 66 | let(:options) { { :title => "Test5 Line Chart", :width => 500, :height => 200, :color => '#437E9D', :legend => "none", :showCategoryLabels => true, :showValueLabels => true, :min => 200, :max => 2000, :valueLabelsInterval => 200 } } 67 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chs=500x200&chxl=0%3A%7C2004%7C2005%7C2006%7C2007%7C&cht=lc&chtt=Test5+Line+Chart&chco=437E9D&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500&chxr=1%2C200%2C2000%2C200&chds=200%2C2000") } 68 | end 69 | end 70 | 71 | describe GoogleVisualr::Image::BarChart do 72 | let(:chart) { GoogleVisualr::Image::BarChart.new( data_table, options ) } 73 | 74 | it_should_behave_like "image chart" do 75 | let(:options) { { :title => "Test1 Bar Chart", :colors => ['#437E9D', '#E6A65A'], :legend => "left", :backgroundColor => "#EFEFEF", :showCategoryLabels => true, :showValueLabels => false } } 76 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chf=bg%2Cs%2CEFEFEF&chdl=Sales%7CExpenses&chs=400x200&chxl=1%3A%7C2004%7C2005%7C2006%7C2007%7C0%3A%7C%7C&cht=bhs&chdlp=l&chtt=Test1+Bar+Chart&chco=437E9D%2CE6A65A&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500&chds=a") } 77 | end 78 | 79 | it_should_behave_like "image chart" do 80 | let(:options) { { :title => "Test2 Bar Chart", :width => 500, :height => 200, :color => '#437E9D', :legend => "right", :backgroundColor => "#FFFFFF", :showCategoryLabels => false, :showValueLabels => true } } 81 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chf=bg%2Cs%2CFFFFFF&chdl=Sales%7CExpenses&chs=500x200&chxl=1%3A%7C%7C&cht=bhs&chdlp=r&chtt=Test2+Bar+Chart&chco=437E9D&chds=a&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500") } 82 | end 83 | 84 | it_should_behave_like "image chart" do 85 | let(:options) { { :title => "Test3 Bar Chart", :width => 500, :height => 200, :colors => ['#437E9D', '#E6A65A'], :legend => "top", :showCategoryLabels => false, :showValueLabels => false } } 86 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chdl=Sales%7CExpenses&chs=500x200&chxl=1%3A%7C%7C0%3A%7C%7C&cht=bhs&chdlp=t&chtt=Test3+Bar+Chart&chco=437E9D%2CE6A65A&chds=a&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500") } 87 | end 88 | 89 | it_should_behave_like "image chart" do 90 | let(:options) { { :title => "Test4 Bar Chart", :width => 500, :height => 200, :colors => ['#437E9D', '#E6A65A'], :legend => "bottom", :showCategoryLabels => true, :showValueLabels => true, :isStacked => false, :isVertical => true } } 91 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chdl=Sales%7CExpenses&chs=500x200&chxl=0%3A%7C2004%7C2005%7C2006%7C2007%7C&cht=bvg&chdlp=b&chtt=Test4+Bar+Chart&chco=437E9D%2CE6A65A&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500&chds=a") } 92 | end 93 | 94 | it_should_behave_like "image chart" do 95 | let(:options) { { :title => "Test5 Bar Chart", :width => 500, :height => 200, :colors => ['#437E9D', '#E6A65A'], :legend => "none", :isStacked => true, :isVertical => true, :min => 200, :max => 2000, :valueLabelsInterval => 200 } } 96 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chs=500x200&chxl=0%3A%7C2004%7C2005%7C2006%7C2007%7C&cht=bvs&chtt=Test5+Bar+Chart&chco=437E9D%2CE6A65A&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500&chxr=1%2C200%2C2000%2C200&chds=200%2C2000") } 97 | end 98 | end 99 | 100 | describe GoogleVisualr::Image::SparkLine do 101 | let(:chart) { GoogleVisualr::Image::SparkLine.new( data_table, options ) } 102 | 103 | it_should_behave_like "image chart" do 104 | let(:options) { { :title => "Test1 Sparkline", :colors => ['#437E9D', '#E6A65A'], :legend => "left", :backgroundColor => "#EFEFEF", :showAxisLines => false } } 105 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chf=bg%2Cs%2CEFEFEF&chdl=Sales%7CExpenses&chs=400x200&chxl=0%3A%7C%7C1%3A%7C%7C&cht=ls&chdlp=l&chtt=Test1+Sparkline&chco=437E9D%2CE6A65A&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500&chds=a") } 106 | end 107 | 108 | it_should_behave_like "image chart" do 109 | let(:options) { { :title => "Test2 Sparkline", :width => 500, :height => 200, :color => '#437E9D', :legend => "right", :backgroundColor => "#FFFFFF", :showAxisLines => true } } 110 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chf=bg%2Cs%2CFFFFFF&chdl=Sales%7CExpenses&chs=500x200&chxl=0%3A%7C%7C&cht=ls&chdlp=r&chtt=Test2+Sparkline&chco=437E9D&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500&chds=a") } 111 | end 112 | 113 | it_should_behave_like "image chart" do 114 | let(:options) { { :title => "Test3 Sparkline", :width => 500, :height => 200, :colors => ['#437E9D', '#E6A65A'], :legend => "top", :showAxisLines => false } } 115 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chdl=Sales%7CExpenses&chs=500x200&chxl=0%3A%7C%7C1%3A%7C%7C&cht=ls&chdlp=t&chtt=Test3+Sparkline&chco=437E9D%2CE6A65A&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500&chds=a") } 116 | end 117 | 118 | it_should_behave_like "image chart" do 119 | let(:options) { { :title => "Test4 Sparkline", :width => 500, :height => 200, :color => '#437E9D', :legend => "bottom", :showAxisLines => false } } 120 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chdl=Sales%7CExpenses&chs=500x200&chxl=0%3A%7C%7C1%3A%7C%7C&cht=ls&chdlp=b&chtt=Test4+Sparkline&chco=437E9D&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500&chds=a") } 121 | end 122 | 123 | it_should_behave_like "image chart" do 124 | let(:options) { { :title => "Test5 Sparkline", :width => 500, :height => 200, :colors => ['#437E9D', '#E6A65A'], :legend => "none", :showAxisLines => false } } 125 | let(:uri) { URI.parse("https://chart.googleapis.com/chart?chxt=x%2Cy&chs=500x200&chxl=0%3A%7C%7C1%3A%7C%7C&cht=ls&chtt=Test5+Sparkline&chco=437E9D%2CE6A65A&chd=t%3A1000%2C1200%2C1500%2C800%7C400%2C450%2C600%2C500&chds=a") } 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /spec/google_visualr/data_table_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GoogleVisualr::DataTable do 4 | 5 | let(:dt) { GoogleVisualr::DataTable.new } 6 | 7 | def valid_object 8 | @cols = [ 9 | { :id => 'A', :label => 'NEW A' , :type => 'string' }, 10 | { :id => 'B', :label => 'B-label', :type => 'number' }, 11 | { :id => 'C', :label => 'C-label', :type => 'date' } 12 | ] 13 | @rows = [ 14 | { :c => [ {:v => 'a'}, {:v => 1.0, :f => 'One'} , {:v => Date.parse('2008-02-28 00:31:26'), :f => '2/28/08 12:31 AM'} ] }, 15 | { :c => [ {:v => 'b'}, {:v => 2.0, :f => 'Two'} , {:v => Date.parse('2008-03-30 00:31:26'), :f => '3/30/08 12:31 AM'} ] }, 16 | { :c => [ {:v => 'c'}, {:v => 3.0, :f => 'Three'}, {:v => Date.parse('2008-04-30 00:31:26'), :f => '4/30/08 12:31 AM'} ] } 17 | ] 18 | GoogleVisualr::DataTable.new({:cols => @cols, :rows => @rows}) 19 | end 20 | 21 | describe "#new" do 22 | it "initializes without params" do 23 | dt.should_not be_nil 24 | dt.cols.should be_a_kind_of Array 25 | dt.rows.should be_a_kind_of Array 26 | end 27 | 28 | it "initializes with params" do 29 | dt = valid_object 30 | 31 | @cols.size.times do |i| 32 | dt.cols[i].should == @cols[i] 33 | end 34 | 35 | @rows.size.times do |i| 36 | @cols.size.times do |j| 37 | cell = dt.rows[i][j] 38 | cell.should be_a_kind_of GoogleVisualr::DataTable::Cell 39 | cell.v.should == @rows[i][:c][j][:v] 40 | cell.f.should == @rows[i][:c][j][:f] 41 | end 42 | end 43 | end 44 | end 45 | 46 | describe "#new_column" do 47 | it "initializes a new column with only type param" do 48 | dt.new_column('string') 49 | dt.cols.first.should == {:type => 'string'} 50 | end 51 | 52 | it "initializes a new column with all params" do 53 | dt.new_column('string', 'A LABEL', 'col_0') 54 | dt.cols.first.should == {:id => 'col_0', :label => 'A LABEL', :type => 'string'} 55 | end 56 | 57 | it "initializes a new column with experimental role param" do 58 | dt.new_column('string', nil, nil, 'interval', 'pattern') 59 | dt.cols.first.should == {:type => 'string', :role => 'interval', :pattern => 'pattern'} 60 | end 61 | end 62 | 63 | describe "new_columns" do 64 | it "initializes new columns (with experimental support)" do 65 | columns = [ 66 | {:id => 'A', :label => 'NEW A', :type => 'string'}, 67 | {:id => 'B', :label => 'NEW B', :type => 'string'}, 68 | {:type => 'string', :role => 'interval', :pattern => 'pattern'} 69 | ] 70 | 71 | dt.new_columns(columns) 72 | dt.cols[0].should == columns[0] 73 | dt.cols[1].should == columns[1] 74 | dt.cols[2].should == columns[2] 75 | end 76 | end 77 | 78 | context "column values" do 79 | before do 80 | dt.new_column({:type => 'number'}) 81 | dt.set_column(0, [1,2,3]) 82 | end 83 | 84 | describe "#set_column" do 85 | it "sets a column of values to column #index" do 86 | dt.rows[0][0].v.should == 1 87 | dt.rows[1][0].v.should == 2 88 | dt.rows[2][0].v.should == 3 89 | end 90 | end 91 | 92 | describe "#get_column" do 93 | it "retrieves values in column #index" do 94 | dt.get_column(0).should == [1,2,3] 95 | end 96 | end 97 | end 98 | 99 | context "row values" do 100 | before do 101 | dt.new_columns( [ {:type => 'number'}, {:type => 'string'} ] ) 102 | dt.rows.should be_empty 103 | end 104 | 105 | describe "#add_row" do 106 | context "when param is empty" do 107 | it "adds an empty row to the data_table" do 108 | dt.add_row 109 | dt.rows.size.should == 1 110 | dt.rows[0].should be_empty 111 | end 112 | end 113 | 114 | context "when param is not empty" do 115 | it "adds the row values to the data_table" do 116 | dt.add_row([1, 'A']) 117 | dt.rows.size.should == 1 118 | dt.rows[0][0].v.should == 1 119 | dt.rows[0][1].v.should == 'A' 120 | end 121 | end 122 | end 123 | 124 | describe "#add_rows" do 125 | context "when param is number" do 126 | it "adds x number of empty rows to the data_table" do 127 | dt.add_rows(2) 128 | dt.rows.size.should == 2 129 | dt.rows[0].should be_empty 130 | dt.rows[1].should be_empty 131 | end 132 | end 133 | 134 | context "when param is an array" do 135 | it "adds the rows to the data_table" do 136 | dt.add_rows( [ [1, 'A'], [2, 'B'] ] ) 137 | dt.rows.size.should == 2 138 | 139 | dt.rows[0][0].v.should == 1 140 | dt.rows[0][1].v.should == 'A' 141 | dt.rows[1][0].v.should == 2 142 | dt.rows[1][1].v.should == 'B' 143 | end 144 | end 145 | end 146 | 147 | describe "@get_row" do 148 | it "retrieves values in row #index" do 149 | dt.add_rows( [ [1, 'A'], [2, 'B'] ] ) 150 | dt.rows.size.should == 2 151 | 152 | dt.get_row(0).should == [1, 'A'] 153 | dt.get_row(1).should == [2, 'B'] 154 | end 155 | end 156 | end 157 | 158 | context "cell value" do 159 | before do 160 | dt.new_columns( [ {:type => 'string'}, {:type => 'number'}, {:type => 'boolean'}, {:type => 'datetime'}, {:type => 'date'} ] ) 161 | dt.add_row 162 | end 163 | 164 | describe "#set_cell" do 165 | it "sets cell" do 166 | dt.set_cell(0, 0, {:v => 'ABCD'}) 167 | dt.set_cell(0, 1, 1000) 168 | 169 | dt.get_row(0).should == ['ABCD', 1000] 170 | end 171 | 172 | it "raises an exception if the row_index or column_index specified is out of range" do 173 | expect { 174 | dt.set_cell(5, 0, 1000) 175 | }.to raise_exception(RangeError) 176 | 177 | expect { 178 | dt.set_cell(0, 5, 1000) 179 | }.to raise_exception(RangeError) 180 | end 181 | 182 | describe "#verify_against_column_type" do 183 | def assert_raises_exception(col, value) 184 | expect { 185 | dt.set_cell(0, col, value) 186 | }.to raise_exception(ArgumentError) 187 | end 188 | 189 | it "raises an exception if value is not string" do 190 | assert_raises_exception(0, 1234) 191 | end 192 | 193 | it "raises an exception if value is not number" do 194 | assert_raises_exception(1, 'ABCD') 195 | end 196 | 197 | it "raises an exception if value is not boolean" do 198 | assert_raises_exception(2, 'ABCD') 199 | end 200 | 201 | it "raises an exception if value is not datetime or time" do 202 | assert_raises_exception(3, 'ABCD') 203 | end 204 | 205 | it "raises an exception if value is not date" do 206 | assert_raises_exception(4, 'ABCD') 207 | end 208 | 209 | it "accepts BigDecimal as number" do 210 | expect { 211 | dt.set_cell(0, 1, BigDecimal.new(42)) 212 | }.to_not raise_exception 213 | end 214 | end 215 | 216 | it "accepts 'nil' for all column types" do 217 | expect { 218 | dt.set_cell(0, 0, nil) 219 | }.to_not raise_exception 220 | end 221 | end 222 | 223 | describe "#get_cell" do 224 | it "gets cell" do 225 | dt.set_cell(0, 0, 'ABCD') 226 | dt.get_cell(0, 0).should == 'ABCD' 227 | end 228 | 229 | it "raises an exception if the row_index or column_index specified is out of range" do 230 | expect { 231 | dt.get_cell(0, 5) 232 | }.to raise_exception(RangeError) 233 | 234 | expect { 235 | dt.get_cell(5, 0) 236 | }.to raise_exception(RangeError) 237 | end 238 | end 239 | end 240 | 241 | describe "#to_js" do 242 | context "cols" do 243 | it "includes :id and :label when these are specified" do 244 | dt.new_column('number', 'Total', '1') 245 | dt.add_row([1]) 246 | 247 | dt.to_js.should == "var data_table = new google.visualization.DataTable();data_table.addColumn({\"type\":\"number\",\"label\":\"Total\",\"id\":\"1\"});data_table.addRow([{v: 1}]);" 248 | end 249 | 250 | it "excludes :id and :label when these are not specified" do 251 | dt.new_column('number') 252 | dt.add_row([1]) 253 | 254 | dt.to_js.should == "var data_table = new google.visualization.DataTable();data_table.addColumn({\"type\":\"number\"});data_table.addRow([{v: 1}]);" 255 | end 256 | 257 | it "includes :role and :pattern when these are specified" do 258 | dt.new_column('string', nil, nil, 'interval', 'pattern') 259 | dt.add_row(['interval']) 260 | 261 | dt.to_js.should == "var data_table = new google.visualization.DataTable();data_table.addColumn({\"type\":\"string\",\"role\":\"interval\",\"pattern\":\"pattern\"});data_table.addRow([{v: \"interval\"}]);" 262 | end 263 | 264 | it "escapes labels with apostrophes properly" do 265 | dt.new_column('number', 'Winston\'s') 266 | dt.add_row([1]) 267 | 268 | dt.to_js.should == "var data_table = new google.visualization.DataTable();data_table.addColumn({\"type\":\"number\",\"label\":\"Winston's\"});data_table.addRow([{v: 1}]);" 269 | end 270 | end 271 | 272 | context "valid object literal" do 273 | it "converts object to js string" do 274 | dt = valid_object 275 | js = dt.to_js 276 | js.should match /google.visualization.DataTable/i 277 | js.should match /addColumn/i 278 | js.should match /addRow/i 279 | end 280 | end 281 | end 282 | 283 | describe "Cell" do 284 | describe "#new" do 285 | it "initializes with a value" do 286 | cell = GoogleVisualr::DataTable::Cell.new(1) 287 | cell.v.should == 1 288 | end 289 | 290 | it "initializes with a hash" do 291 | cell = GoogleVisualr::DataTable::Cell.new( { :v => 1, :f => "1.0", :p => {:style => "border: 1px solid green;"} } ) 292 | cell.v.should == 1 293 | cell.f.should == "1.0" 294 | cell.p.should == {:style => "border: 1px solid green;"} 295 | end 296 | end 297 | 298 | describe "#to_js" do 299 | context "initialized with a value" do 300 | it "returns a json string" do 301 | cell = GoogleVisualr::DataTable::Cell.new(1) 302 | cell.to_js.should == "{v: 1}" 303 | end 304 | 305 | it "returns 'null' when v is nil" do 306 | cell = GoogleVisualr::DataTable::Cell.new(nil) 307 | cell.to_js.should == "null" 308 | end 309 | end 310 | 311 | context "initialized with a hash" do 312 | it "returns a json string when v is present" do 313 | cell = GoogleVisualr::DataTable::Cell.new( { :v => 1, :f => "1.0", :p => {:style => "border: 1px solid green;"} } ) 314 | cell.to_js.should == "{v: 1, f: \"1.0\", p: {style: \"border: 1px solid green;\"}}" 315 | end 316 | 317 | it "returns a json string when v is nil" do 318 | cell = GoogleVisualr::DataTable::Cell.new( { :v => nil, :f => "-", :p => {:style => "border: 1px solid red;"} } ) 319 | cell.to_js.should == "{v: null, f: \"-\", p: {style: \"border: 1px solid red;\"}}" 320 | end 321 | 322 | it "returns a valid json string when there are apostrophes in v or f" do 323 | cell = GoogleVisualr::DataTable::Cell.new( { :v => "I'm \"Winston\"", :f => "Winston
Nice Guy
" } ) 324 | expected = "{v: \"I'm \\\"Winston\\\"\", f: \"Winston
Nice Guy
\"}" 325 | 326 | expect(normalize_javascript(cell.to_js)).to eq expected 327 | end 328 | end 329 | end 330 | end 331 | end 332 | -------------------------------------------------------------------------------- /lib/google_visualr/data_table.rb: -------------------------------------------------------------------------------- 1 | module GoogleVisualr 2 | 3 | class DataTable 4 | 5 | attr_accessor :cols 6 | attr_accessor :rows 7 | 8 | ############################## 9 | # Constructors 10 | ############################## 11 | # 12 | # GoogleVisualr::DataTable.new 13 | # Creates an empty data_table instance. Use new_column/s, add_row/s and set_cell methods to populate the data_table. 14 | # 15 | # GoogleVisualr::DataTable.new(data_object) 16 | # Creates a data_table by passing a JavaScript-string-literal like data object into the data parameter. This object can contain formatting options. 17 | # 18 | ############################## 19 | # Syntax Description of a Data Object 20 | ############################## 21 | # 22 | # The data object consists of two required top-level properties, cols and rows. 23 | # 24 | # * cols property 25 | # 26 | # cols is an array of objects describing the ID and type of each column. Each property is an object with the following properties (case-sensitive): 27 | # 28 | # * type [Required] The data type of the data in the column. Supports the following string values: 29 | # - 'string' : String value. Example values: v:'foo', :v:'bar' 30 | # - 'number' : Number value. Example values: v:7, v:3.14, v:-55 31 | # - 'boolean' : Boolean value ('true' or 'false'). Example values: v:true, v:false 32 | # - 'date' : Date object, with the time truncated. Example value: v:Date.parse('2010-01-01') 33 | # - 'datetime' : DateTime/Time object, time inclusive. Example value: v:DateTime.parse('2010-01-01 14:20:25') 34 | # - 'timeofday' : Array of 3 numbers or 4 numbers, [Hour,Minute,Second,(Optional) Milliseconds]. Example value: v:[8, 15, 0] 35 | # * label [Optional] A string value that some visualizations display for this column. Example: label:'Height' 36 | # * id [Optional] A unique (basic alphanumeric) string ID of the column. Be careful not to choose a JavaScript keyword. Example: id:'col_1' 37 | # 38 | # * rows property 39 | # 40 | # The rows property holds an array of row objects. Each row object has one required property called c, which is an array of cells in that row. 41 | # 42 | # Each cell in the table is described by an object with the following properties: 43 | # 44 | # * v [Optional] The cell value. The data type should match the column data type. 45 | # * f [Optional] A string version of the v value, formatted strictly for display only. If omitted, a string version of v will be used. 46 | # * p [Optional] An object that is a map of custom values applied to the cell. Example: :p => { :style => 'border: 1px solid green;' }. 47 | # 48 | # Cells in the row array should be in the same order as their column descriptions in cols. 49 | # 50 | # To indicate a null cell, you can either specify null, or set empty string for a cell in an array, or omit trailing array members. 51 | # So, to indicate a row with null for the first two cells, you could specify [ '', '', {cell_val}] or [null, null, {cell_val}]. 52 | def initialize(options = {}) 53 | @cols = Array.new 54 | @rows = Array.new 55 | 56 | unless options.empty? 57 | cols = options[:cols] 58 | new_columns(cols) 59 | 60 | rows = options[:rows] 61 | rows.each do |row| 62 | add_row(row[:c]) 63 | end 64 | end 65 | end 66 | 67 | # Adds a new column to the data_table. 68 | # Experimental support for role (and pattern): http://code.google.com/apis/chart/interactive/docs/roles.html. 69 | # 70 | # Parameters: 71 | # * type [Required] The data type of the data in the column. Supports the following string values: 72 | # - 'string' : String value. Example values: v:'hello' 73 | # - 'number' : Number value. Example values: v:7 , v:3.14, v:-55 74 | # - 'date' : Date object, with the time truncated. Example values: v:Date.parse('2010-01-01') 75 | # - 'datetime' : Date object including the time. Example values: v:Date.parse('2010-01-01 14:20:25') 76 | # - 'boolean' : Boolean value ('true' or 'false'). Example values: v: true 77 | # * label [Optional] A string value that some visualizations display for this column. Example: label:'Height' 78 | # * id [Optional] A unique (basic alphanumeric) string ID of the column. Be careful not to choose a JavaScript keyword. Example: id:'col_1' 79 | # * role [Optional] A string value that describes the purpose of the data in that column. Example, a column might hold data describing tooltip text, data point annotations, or uncertainty indicators. 80 | # * pattern [Optional] A number (or date) format string specifying how to display the column value; used in conjunction with role. 81 | def new_column(type, label=nil, id =nil, role=nil, pattern=nil) 82 | column = { :type => type, :label => label, :id => id, :role => role, :pattern => pattern }.reject { |key, value| value.nil? } 83 | @cols << column 84 | end 85 | 86 | # Adds multiple columns to the data_table. 87 | # 88 | # Parameters: 89 | # * columns [Required] An array of column objects {:type, :label, :id, :role, :pattern}. Calls new_column for each column object. 90 | def new_columns(columns) 91 | columns.each do |column| 92 | new_column(column[:type], column[:label], column[:id], column[:role], column[:pattern]) 93 | end 94 | end 95 | 96 | # Sets a column in data_table, specified by column_index with an array of column_values. column_index starts from 0. 97 | # 98 | # Parameters 99 | # * column_index [Required] The column to assign column_values. column_index starts from 0. 100 | # * column_values [Required] An array of cell values. 101 | def set_column(column_index, column_values) 102 | if @rows.size < column_values.size 103 | 1.upto(column_values.size - @rows.size) { @rows << Array.new } 104 | end 105 | 106 | column_values.each_with_index do |column_value, row_index| 107 | set_cell(row_index, column_index, column_value) 108 | end 109 | end 110 | 111 | # Gets a column of cell values from the data_table, at column_index. column_index starts from 0. 112 | # 113 | # Parameters 114 | # * column_index [Required] The column to retrieve column values. column_index starts from 0. 115 | def get_column(column_index) 116 | @rows.transpose[column_index].collect(&:v) 117 | end 118 | 119 | # Adds a new row to the data_table. 120 | # Call method without any parameters to add an empty row, otherwise, call method with a row object. 121 | # 122 | # Parameters: 123 | # * row [Optional] An array of cell values specifying the data for the new row. 124 | # - You can specify a value for a cell (e.g. 'hi') or specify a formatted value using cell objects (e.g. {v:55, f:'Fifty-five'}) as described in the constructor section. 125 | # - You can mix simple values and cell objects in the same method call. 126 | # - To create an empty cell, use nil or empty string. 127 | def add_row(row_values=[]) 128 | @rows << Array.new 129 | row_index = @rows.size-1 130 | 131 | row_values.each_with_index do |row_value, column_index| 132 | set_cell(row_index, column_index, row_value) 133 | end 134 | end 135 | 136 | # Adds multiple rows to the data_table. You can call this method with data to populate a set of new rows or create new empty rows. 137 | # 138 | # Parameters: 139 | # * array_or_num [Required] Either an array or a number. 140 | # - Array: An array of row objects used to populate a set of new rows. Each row is an object as described in add_row(). 141 | # - Number: A number specifying the number of new empty rows to create. 142 | def add_rows(array_or_num) 143 | if array_or_num.is_a?(Array) 144 | array_or_num.each do |row| 145 | add_row(row) 146 | end 147 | else 148 | array_or_num.times do 149 | add_row 150 | end 151 | end 152 | end 153 | 154 | # Gets a row of cell values from the data_table, at row_index. row_index starts from 0. 155 | # 156 | # Parameters 157 | # * row_index [Required] The row to retrieve row values. row_index starts from 0. 158 | def get_row(row_index) 159 | @rows[row_index].collect(&:v) 160 | end 161 | 162 | # Sets the value (and formatted value) of a cell. 163 | # 164 | # Parameters: 165 | # * row_index [Required] A number greater than or equal to zero, but smaller than the total number of rows. 166 | # * column_index [Required] A number greater than or equal to zero, but smaller than the total number of columns. 167 | # * value [Required] The cell value. 168 | # The data type should match the column data type. 169 | # You can specify a value for a cell (e.g. 'hi'). 170 | # Or specify a formatted value using cell objects (e.g. {v:55, f:'Fifty-five'}). 171 | def set_cell(row_index, column_index, value) 172 | if within_range?(row_index, column_index) 173 | verify_against_column_type( @cols[column_index][:type], value ) 174 | @rows[row_index][column_index] = GoogleVisualr::DataTable::Cell.new(value, @cols[column_index][:type]) 175 | else 176 | raise RangeError, "row_index and column_index MUST be < @rows.size and @cols.size", caller 177 | end 178 | end 179 | 180 | # Gets a cell value from the visualization, at row_index, column_index. row_index and column_index start from 0. 181 | # 182 | # Parameters: 183 | # * row_index [Required] row_index starts from 0. 184 | # * column_index [Required] column_index starts from 0. 185 | def get_cell(row_index, column_index) 186 | if within_range?(row_index, column_index) 187 | @rows[row_index][column_index].v 188 | else 189 | raise RangeError, "row_index and column_index MUST be < @rows.size and @cols.size", caller 190 | end 191 | end 192 | 193 | # Applies one or more formatters to the data_table to format the columns as specified by the formatter/s. 194 | # 195 | # Parameters: 196 | # * formatter/s [Required] One, or an array of formatters. 197 | def format(*formatters) 198 | @formatters ||= Array.new 199 | @formatters += formatters 200 | end 201 | 202 | # Returns the JavaScript equivalent for this data_table instance. 203 | def to_js 204 | js = "var data_table = new google.visualization.DataTable();" 205 | 206 | @cols.each do |column| 207 | js << "data_table.addColumn(" 208 | js << display(column) 209 | js << ");" 210 | end 211 | 212 | @rows.each do |row| 213 | js << "data_table.addRow(" 214 | js << "[#{row.map(&:to_js).join(", ")}]" unless row.empty? 215 | js << ");" 216 | end 217 | 218 | if @formatters 219 | @formatters.each do |formatter| 220 | js << formatter.to_js 221 | end 222 | end 223 | 224 | js 225 | end 226 | 227 | private 228 | 229 | def display(column) 230 | column[:type] = "datetime" if column[:type] == "time" 231 | column.to_json 232 | end 233 | 234 | def within_range?(row_index, column_index) 235 | row_index < @rows.size && column_index < @cols.size 236 | end 237 | 238 | def verify_against_column_type(type, value) 239 | v = value.is_a?(Hash) ? value[:v] : value 240 | 241 | case 242 | when v.nil? 243 | return 244 | when type == "string" 245 | raise ArgumentError, "cell value '#{v}' is not a String", caller unless v.is_a?(String) 246 | when type == "number" 247 | raise ArgumentError, "cell value '#{v}' is not an Integer, Float or BigDecimal", caller unless v.is_a?(Integer) || v.is_a?(Float) || v.is_a?(BigDecimal) 248 | when type == "boolean" 249 | raise ArgumentError, "cell value '#{v}' is not a Boolean", caller unless v.is_a?(TrueClass) || v.is_a?(FalseClass) 250 | when type == 'datetime' 251 | raise ArgumentError, "cell value '#{v}' is not a DateTime", caller unless v.is_a?(DateTime) || v.is_a?(Time) 252 | when type == 'time' 253 | raise ArgumentError, "cell value '#{v}' is not a DateTime", caller unless v.is_a?(DateTime) || v.is_a?(Time) 254 | when type == "date" 255 | raise ArgumentError, "cell value '#{v}' is not a Date", caller unless v.is_a?(Date) 256 | end 257 | end 258 | 259 | class Cell 260 | include GoogleVisualr::ParamHelpers 261 | 262 | attr_accessor :v # value 263 | attr_accessor :f # formatted 264 | attr_accessor :p # properties 265 | 266 | def initialize(options, type = nil) 267 | if options.is_a?(Hash) 268 | @v = options[:v] 269 | @f = options[:f] 270 | @p = options[:p] 271 | @type = type 272 | else # should be a string 273 | @v = options 274 | @type = type 275 | end 276 | end 277 | 278 | def to_js 279 | return "null" if @v.nil? && @f.nil? && @p.nil? 280 | 281 | js = "{" 282 | js << "v: #{typecast(@v, @type)}" 283 | js << ", f: #{typecast(@f)}" unless @f.nil? 284 | js << ", p: #{typecast(@p)}" unless @p.nil? 285 | js << "}" 286 | 287 | js 288 | end 289 | 290 | end 291 | 292 | end 293 | 294 | end 295 | -------------------------------------------------------------------------------- /spec/turbolinks_tests/turbolinks.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var CSRFToken, anchoredLink, browserCompatibleDocumentParser, browserIsntBuggy, browserSupportsPushState, browserSupportsTurbolinks, cacheCurrentPage, cacheSize, changePage, constrainPageCacheTo, createDocument, crossOriginLink, currentState, executeScriptTags, extractLink, extractTitleAndBody, fetchHistory, fetchReplacement, handleClick, ignoreClick, initializeTurbolinks, installClickHandlerLast, installDocumentReadyPageEventTriggers, installHistoryChangeHandler, installJqueryAjaxSuccessPageUpdateTrigger, loadedAssets, noTurbolink, nonHtmlLink, nonStandardClick, pageCache, pageChangePrevented, pagesCached, popCookie, processResponse, recallScrollPosition, referer, reflectNewUrl, reflectRedirectedUrl, rememberCurrentState, rememberCurrentUrl, rememberReferer, removeHash, removeHashForIE10compatiblity, removeNoscriptTags, requestMethodIsSafe, resetScrollPosition, targetLink, triggerEvent, visit, xhr, _ref, 3 | __hasProp = {}.hasOwnProperty, 4 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 5 | 6 | pageCache = {}; 7 | 8 | cacheSize = 10; 9 | 10 | currentState = null; 11 | 12 | loadedAssets = null; 13 | 14 | referer = null; 15 | 16 | createDocument = null; 17 | 18 | xhr = null; 19 | 20 | fetchReplacement = function(url) { 21 | rememberReferer(); 22 | cacheCurrentPage(); 23 | triggerEvent('page:fetch', { 24 | url: url 25 | }); 26 | if (xhr != null) { 27 | xhr.abort(); 28 | } 29 | xhr = new XMLHttpRequest; 30 | xhr.open('GET', removeHashForIE10compatiblity(url), true); 31 | xhr.setRequestHeader('Accept', 'text/html, application/xhtml+xml, application/xml'); 32 | xhr.setRequestHeader('X-XHR-Referer', referer); 33 | xhr.onload = function() { 34 | var doc; 35 | triggerEvent('page:receive'); 36 | if (doc = processResponse()) { 37 | reflectNewUrl(url); 38 | reflectRedirectedUrl(); 39 | changePage.apply(null, extractTitleAndBody(doc)); 40 | resetScrollPosition(); 41 | return triggerEvent('page:load'); 42 | } else { 43 | return document.location.href = url; 44 | } 45 | }; 46 | xhr.onloadend = function() { 47 | return xhr = null; 48 | }; 49 | xhr.onabort = function() { 50 | return rememberCurrentUrl(); 51 | }; 52 | xhr.onerror = function() { 53 | return document.location.href = url; 54 | }; 55 | return xhr.send(); 56 | }; 57 | 58 | fetchHistory = function(cachedPage) { 59 | cacheCurrentPage(); 60 | if (xhr != null) { 61 | xhr.abort(); 62 | } 63 | changePage(cachedPage.title, cachedPage.body); 64 | recallScrollPosition(cachedPage); 65 | return triggerEvent('page:restore'); 66 | }; 67 | 68 | cacheCurrentPage = function() { 69 | pageCache[currentState.position] = { 70 | url: document.location.href, 71 | body: document.body, 72 | title: document.title, 73 | positionY: window.pageYOffset, 74 | positionX: window.pageXOffset 75 | }; 76 | return constrainPageCacheTo(cacheSize); 77 | }; 78 | 79 | pagesCached = function(size) { 80 | if (size == null) { 81 | size = cacheSize; 82 | } 83 | if (/^[\d]+$/.test(size)) { 84 | return cacheSize = parseInt(size); 85 | } 86 | }; 87 | 88 | constrainPageCacheTo = function(limit) { 89 | var key, value; 90 | for (key in pageCache) { 91 | if (!__hasProp.call(pageCache, key)) continue; 92 | value = pageCache[key]; 93 | if (key <= currentState.position - limit) { 94 | pageCache[key] = null; 95 | } 96 | } 97 | }; 98 | 99 | changePage = function(title, body, csrfToken, runScripts) { 100 | document.title = title; 101 | document.documentElement.replaceChild(body, document.body); 102 | if (csrfToken != null) { 103 | CSRFToken.update(csrfToken); 104 | } 105 | removeNoscriptTags(); 106 | if (runScripts) { 107 | executeScriptTags(); 108 | } 109 | currentState = window.history.state; 110 | triggerEvent('page:change'); 111 | return triggerEvent('page:update'); 112 | }; 113 | 114 | executeScriptTags = function() { 115 | var attr, copy, nextSibling, parentNode, script, scripts, _i, _j, _len, _len1, _ref, _ref1; 116 | scripts = Array.prototype.slice.call(document.body.querySelectorAll('script:not([data-turbolinks-eval="false"])')); 117 | for (_i = 0, _len = scripts.length; _i < _len; _i++) { 118 | script = scripts[_i]; 119 | if (!((_ref = script.type) === '' || _ref === 'text/javascript')) { 120 | continue; 121 | } 122 | copy = document.createElement('script'); 123 | _ref1 = script.attributes; 124 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 125 | attr = _ref1[_j]; 126 | copy.setAttribute(attr.name, attr.value); 127 | } 128 | copy.appendChild(document.createTextNode(script.innerHTML)); 129 | parentNode = script.parentNode, nextSibling = script.nextSibling; 130 | parentNode.removeChild(script); 131 | parentNode.insertBefore(copy, nextSibling); 132 | } 133 | }; 134 | 135 | removeNoscriptTags = function() { 136 | var noscript, noscriptTags, _i, _len; 137 | noscriptTags = Array.prototype.slice.call(document.body.getElementsByTagName('noscript')); 138 | for (_i = 0, _len = noscriptTags.length; _i < _len; _i++) { 139 | noscript = noscriptTags[_i]; 140 | noscript.parentNode.removeChild(noscript); 141 | } 142 | }; 143 | 144 | reflectNewUrl = function(url) { 145 | if (url !== referer) { 146 | return window.history.pushState({ 147 | turbolinks: true, 148 | position: currentState.position + 1 149 | }, '', url); 150 | } 151 | }; 152 | 153 | reflectRedirectedUrl = function() { 154 | var location, preservedHash; 155 | if (location = xhr.getResponseHeader('X-XHR-Redirected-To')) { 156 | preservedHash = removeHash(location) === location ? document.location.hash : ''; 157 | return window.history.replaceState(currentState, '', location + preservedHash); 158 | } 159 | }; 160 | 161 | rememberReferer = function() { 162 | return referer = document.location.href; 163 | }; 164 | 165 | rememberCurrentUrl = function() { 166 | return window.history.replaceState({ 167 | turbolinks: true, 168 | position: Date.now() 169 | }, '', document.location.href); 170 | }; 171 | 172 | rememberCurrentState = function() { 173 | return currentState = window.history.state; 174 | }; 175 | 176 | recallScrollPosition = function(page) { 177 | return window.scrollTo(page.positionX, page.positionY); 178 | }; 179 | 180 | resetScrollPosition = function() { 181 | if (document.location.hash) { 182 | return document.location.href = document.location.href; 183 | } else { 184 | return window.scrollTo(0, 0); 185 | } 186 | }; 187 | 188 | removeHashForIE10compatiblity = function(url) { 189 | return removeHash(url); 190 | }; 191 | 192 | removeHash = function(url) { 193 | var link; 194 | link = url; 195 | if (url.href == null) { 196 | link = document.createElement('A'); 197 | link.href = url; 198 | } 199 | return link.href.replace(link.hash, ''); 200 | }; 201 | 202 | popCookie = function(name) { 203 | var value, _ref; 204 | value = ((_ref = document.cookie.match(new RegExp(name + "=(\\w+)"))) != null ? _ref[1].toUpperCase() : void 0) || ''; 205 | document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'; 206 | return value; 207 | }; 208 | 209 | triggerEvent = function(name, data) { 210 | var event; 211 | event = document.createEvent('Events'); 212 | if (data) { 213 | event.data = data; 214 | } 215 | event.initEvent(name, true, true); 216 | return document.dispatchEvent(event); 217 | }; 218 | 219 | pageChangePrevented = function() { 220 | return !triggerEvent('page:before-change'); 221 | }; 222 | 223 | processResponse = function() { 224 | var assetsChanged, clientOrServerError, doc, extractTrackAssets, intersection, validContent; 225 | clientOrServerError = function() { 226 | var _ref; 227 | return (400 <= (_ref = xhr.status) && _ref < 600); 228 | }; 229 | validContent = function() { 230 | return xhr.getResponseHeader('Content-Type').match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/); 231 | }; 232 | extractTrackAssets = function(doc) { 233 | var node, _i, _len, _ref, _results; 234 | _ref = doc.head.childNodes; 235 | _results = []; 236 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 237 | node = _ref[_i]; 238 | if ((typeof node.getAttribute === "function" ? node.getAttribute('data-turbolinks-track') : void 0) != null) { 239 | _results.push(node.src || node.href); 240 | } 241 | } 242 | return _results; 243 | }; 244 | assetsChanged = function(doc) { 245 | var fetchedAssets; 246 | loadedAssets || (loadedAssets = extractTrackAssets(document)); 247 | fetchedAssets = extractTrackAssets(doc); 248 | return fetchedAssets.length !== loadedAssets.length || intersection(fetchedAssets, loadedAssets).length !== loadedAssets.length; 249 | }; 250 | intersection = function(a, b) { 251 | var value, _i, _len, _ref, _results; 252 | if (a.length > b.length) { 253 | _ref = [b, a], a = _ref[0], b = _ref[1]; 254 | } 255 | _results = []; 256 | for (_i = 0, _len = a.length; _i < _len; _i++) { 257 | value = a[_i]; 258 | if (__indexOf.call(b, value) >= 0) { 259 | _results.push(value); 260 | } 261 | } 262 | return _results; 263 | }; 264 | if (!clientOrServerError() && validContent()) { 265 | doc = createDocument(xhr.responseText); 266 | if (doc && !assetsChanged(doc)) { 267 | return doc; 268 | } 269 | } 270 | }; 271 | 272 | extractTitleAndBody = function(doc) { 273 | var title; 274 | title = doc.querySelector('title'); 275 | return [title != null ? title.textContent : void 0, doc.body, CSRFToken.get(doc).token, 'runScripts']; 276 | }; 277 | 278 | CSRFToken = { 279 | get: function(doc) { 280 | var tag; 281 | if (doc == null) { 282 | doc = document; 283 | } 284 | return { 285 | node: tag = doc.querySelector('meta[name="csrf-token"]'), 286 | token: tag != null ? typeof tag.getAttribute === "function" ? tag.getAttribute('content') : void 0 : void 0 287 | }; 288 | }, 289 | update: function(latest) { 290 | var current; 291 | current = this.get(); 292 | if ((current.token != null) && (latest != null) && current.token !== latest) { 293 | return current.node.setAttribute('content', latest); 294 | } 295 | } 296 | }; 297 | 298 | browserCompatibleDocumentParser = function() { 299 | var createDocumentUsingDOM, createDocumentUsingParser, createDocumentUsingWrite, testDoc, _ref; 300 | createDocumentUsingParser = function(html) { 301 | return (new DOMParser).parseFromString(html, 'text/html'); 302 | }; 303 | createDocumentUsingDOM = function(html) { 304 | var doc; 305 | doc = document.implementation.createHTMLDocument(''); 306 | doc.documentElement.innerHTML = html; 307 | return doc; 308 | }; 309 | createDocumentUsingWrite = function(html) { 310 | var doc; 311 | doc = document.implementation.createHTMLDocument(''); 312 | doc.open('replace'); 313 | doc.write(html); 314 | doc.close(); 315 | return doc; 316 | }; 317 | try { 318 | if (window.DOMParser) { 319 | testDoc = createDocumentUsingParser('

test'); 320 | return createDocumentUsingParser; 321 | } 322 | } catch (e) { 323 | testDoc = createDocumentUsingDOM('

test'); 324 | return createDocumentUsingDOM; 325 | } finally { 326 | if ((testDoc != null ? (_ref = testDoc.body) != null ? _ref.childNodes.length : void 0 : void 0) !== 1) { 327 | return createDocumentUsingWrite; 328 | } 329 | } 330 | }; 331 | 332 | installClickHandlerLast = function(event) { 333 | if (!event.defaultPrevented) { 334 | document.removeEventListener('click', handleClick, false); 335 | return document.addEventListener('click', handleClick, false); 336 | } 337 | }; 338 | 339 | handleClick = function(event) { 340 | var link; 341 | if (!event.defaultPrevented) { 342 | link = extractLink(event); 343 | if (link.nodeName === 'A' && !ignoreClick(event, link)) { 344 | if (!pageChangePrevented()) { 345 | visit(link.href); 346 | } 347 | return event.preventDefault(); 348 | } 349 | } 350 | }; 351 | 352 | extractLink = function(event) { 353 | var link; 354 | link = event.target; 355 | while (!(!link.parentNode || link.nodeName === 'A')) { 356 | link = link.parentNode; 357 | } 358 | return link; 359 | }; 360 | 361 | crossOriginLink = function(link) { 362 | return location.protocol !== link.protocol || location.host !== link.host; 363 | }; 364 | 365 | anchoredLink = function(link) { 366 | return ((link.hash && removeHash(link)) === removeHash(location)) || (link.href === location.href + '#'); 367 | }; 368 | 369 | nonHtmlLink = function(link) { 370 | var url; 371 | url = removeHash(link); 372 | return url.match(/\.[a-z]+(\?.*)?$/g) && !url.match(/\.html?(\?.*)?$/g); 373 | }; 374 | 375 | noTurbolink = function(link) { 376 | var ignore; 377 | while (!(ignore || link === document)) { 378 | ignore = link.getAttribute('data-no-turbolink') != null; 379 | link = link.parentNode; 380 | } 381 | return ignore; 382 | }; 383 | 384 | targetLink = function(link) { 385 | return link.target.length !== 0; 386 | }; 387 | 388 | nonStandardClick = function(event) { 389 | return event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey; 390 | }; 391 | 392 | ignoreClick = function(event, link) { 393 | return crossOriginLink(link) || anchoredLink(link) || nonHtmlLink(link) || noTurbolink(link) || targetLink(link) || nonStandardClick(event); 394 | }; 395 | 396 | installDocumentReadyPageEventTriggers = function() { 397 | triggerEvent('page:change'); 398 | return triggerEvent('page:update'); 399 | }; 400 | 401 | installJqueryAjaxSuccessPageUpdateTrigger = function() { 402 | if (typeof jQuery !== 'undefined') { 403 | return $(document).on('ajaxSuccess', function(event, xhr, settings) { 404 | if (!$.trim(xhr.responseText)) { 405 | return; 406 | } 407 | return triggerEvent('page:update'); 408 | }); 409 | } 410 | }; 411 | 412 | installHistoryChangeHandler = function(event) { 413 | var cachedPage, _ref; 414 | if ((_ref = event.state) != null ? _ref.turbolinks : void 0) { 415 | if (cachedPage = pageCache[event.state.position]) { 416 | return fetchHistory(cachedPage); 417 | } else { 418 | return visit(event.target.location.href); 419 | } 420 | } 421 | }; 422 | 423 | initializeTurbolinks = function() { 424 | rememberCurrentUrl(); 425 | rememberCurrentState(); 426 | createDocument = browserCompatibleDocumentParser(); 427 | document.addEventListener('click', installClickHandlerLast, true); 428 | document.addEventListener('DOMContentLoaded', installDocumentReadyPageEventTriggers, true); 429 | installJqueryAjaxSuccessPageUpdateTrigger(); 430 | return window.addEventListener('popstate', installHistoryChangeHandler, false); 431 | }; 432 | 433 | browserSupportsPushState = window.history && window.history.pushState && window.history.replaceState && window.history.state !== void 0; 434 | 435 | browserIsntBuggy = !navigator.userAgent.match(/CriOS\//); 436 | 437 | requestMethodIsSafe = (_ref = popCookie('request_method')) === 'GET' || _ref === ''; 438 | 439 | browserSupportsTurbolinks = browserSupportsPushState && browserIsntBuggy && requestMethodIsSafe; 440 | 441 | if (browserSupportsTurbolinks) { 442 | visit = fetchReplacement; 443 | initializeTurbolinks(); 444 | } else { 445 | visit = function(url) { 446 | return document.location.href = url; 447 | }; 448 | } 449 | 450 | this.Turbolinks = { 451 | visit: visit, 452 | pagesCached: pagesCached, 453 | supported: browserSupportsTurbolinks 454 | }; 455 | }).call(this); -------------------------------------------------------------------------------- /spec/dummy/public/javascripts/dragdrop.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us dragdrop.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 2 | 3 | // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // 5 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 6 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 7 | 8 | if(Object.isUndefined(Effect)) 9 | throw("dragdrop.js requires including script.aculo.us' effects.js library"); 10 | 11 | var Droppables = { 12 | drops: [], 13 | 14 | remove: function(element) { 15 | this.drops = this.drops.reject(function(d) { return d.element==$(element) }); 16 | }, 17 | 18 | add: function(element) { 19 | element = $(element); 20 | var options = Object.extend({ 21 | greedy: true, 22 | hoverclass: null, 23 | tree: false 24 | }, arguments[1] || { }); 25 | 26 | // cache containers 27 | if(options.containment) { 28 | options._containers = []; 29 | var containment = options.containment; 30 | if(Object.isArray(containment)) { 31 | containment.each( function(c) { options._containers.push($(c)) }); 32 | } else { 33 | options._containers.push($(containment)); 34 | } 35 | } 36 | 37 | if(options.accept) options.accept = [options.accept].flatten(); 38 | 39 | Element.makePositioned(element); // fix IE 40 | options.element = element; 41 | 42 | this.drops.push(options); 43 | }, 44 | 45 | findDeepestChild: function(drops) { 46 | deepest = drops[0]; 47 | 48 | for (i = 1; i < drops.length; ++i) 49 | if (Element.isParent(drops[i].element, deepest.element)) 50 | deepest = drops[i]; 51 | 52 | return deepest; 53 | }, 54 | 55 | isContained: function(element, drop) { 56 | var containmentNode; 57 | if(drop.tree) { 58 | containmentNode = element.treeNode; 59 | } else { 60 | containmentNode = element.parentNode; 61 | } 62 | return drop._containers.detect(function(c) { return containmentNode == c }); 63 | }, 64 | 65 | isAffected: function(point, element, drop) { 66 | return ( 67 | (drop.element!=element) && 68 | ((!drop._containers) || 69 | this.isContained(element, drop)) && 70 | ((!drop.accept) || 71 | (Element.classNames(element).detect( 72 | function(v) { return drop.accept.include(v) } ) )) && 73 | Position.within(drop.element, point[0], point[1]) ); 74 | }, 75 | 76 | deactivate: function(drop) { 77 | if(drop.hoverclass) 78 | Element.removeClassName(drop.element, drop.hoverclass); 79 | this.last_active = null; 80 | }, 81 | 82 | activate: function(drop) { 83 | if(drop.hoverclass) 84 | Element.addClassName(drop.element, drop.hoverclass); 85 | this.last_active = drop; 86 | }, 87 | 88 | show: function(point, element) { 89 | if(!this.drops.length) return; 90 | var drop, affected = []; 91 | 92 | this.drops.each( function(drop) { 93 | if(Droppables.isAffected(point, element, drop)) 94 | affected.push(drop); 95 | }); 96 | 97 | if(affected.length>0) 98 | drop = Droppables.findDeepestChild(affected); 99 | 100 | if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); 101 | if (drop) { 102 | Position.within(drop.element, point[0], point[1]); 103 | if(drop.onHover) 104 | drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); 105 | 106 | if (drop != this.last_active) Droppables.activate(drop); 107 | } 108 | }, 109 | 110 | fire: function(event, element) { 111 | if(!this.last_active) return; 112 | Position.prepare(); 113 | 114 | if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) 115 | if (this.last_active.onDrop) { 116 | this.last_active.onDrop(element, this.last_active.element, event); 117 | return true; 118 | } 119 | }, 120 | 121 | reset: function() { 122 | if(this.last_active) 123 | this.deactivate(this.last_active); 124 | } 125 | }; 126 | 127 | var Draggables = { 128 | drags: [], 129 | observers: [], 130 | 131 | register: function(draggable) { 132 | if(this.drags.length == 0) { 133 | this.eventMouseUp = this.endDrag.bindAsEventListener(this); 134 | this.eventMouseMove = this.updateDrag.bindAsEventListener(this); 135 | this.eventKeypress = this.keyPress.bindAsEventListener(this); 136 | 137 | Event.observe(document, "mouseup", this.eventMouseUp); 138 | Event.observe(document, "mousemove", this.eventMouseMove); 139 | Event.observe(document, "keypress", this.eventKeypress); 140 | } 141 | this.drags.push(draggable); 142 | }, 143 | 144 | unregister: function(draggable) { 145 | this.drags = this.drags.reject(function(d) { return d==draggable }); 146 | if(this.drags.length == 0) { 147 | Event.stopObserving(document, "mouseup", this.eventMouseUp); 148 | Event.stopObserving(document, "mousemove", this.eventMouseMove); 149 | Event.stopObserving(document, "keypress", this.eventKeypress); 150 | } 151 | }, 152 | 153 | activate: function(draggable) { 154 | if(draggable.options.delay) { 155 | this._timeout = setTimeout(function() { 156 | Draggables._timeout = null; 157 | window.focus(); 158 | Draggables.activeDraggable = draggable; 159 | }.bind(this), draggable.options.delay); 160 | } else { 161 | window.focus(); // allows keypress events if window isn't currently focused, fails for Safari 162 | this.activeDraggable = draggable; 163 | } 164 | }, 165 | 166 | deactivate: function() { 167 | this.activeDraggable = null; 168 | }, 169 | 170 | updateDrag: function(event) { 171 | if(!this.activeDraggable) return; 172 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 173 | // Mozilla-based browsers fire successive mousemove events with 174 | // the same coordinates, prevent needless redrawing (moz bug?) 175 | if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; 176 | this._lastPointer = pointer; 177 | 178 | this.activeDraggable.updateDrag(event, pointer); 179 | }, 180 | 181 | endDrag: function(event) { 182 | if(this._timeout) { 183 | clearTimeout(this._timeout); 184 | this._timeout = null; 185 | } 186 | if(!this.activeDraggable) return; 187 | this._lastPointer = null; 188 | this.activeDraggable.endDrag(event); 189 | this.activeDraggable = null; 190 | }, 191 | 192 | keyPress: function(event) { 193 | if(this.activeDraggable) 194 | this.activeDraggable.keyPress(event); 195 | }, 196 | 197 | addObserver: function(observer) { 198 | this.observers.push(observer); 199 | this._cacheObserverCallbacks(); 200 | }, 201 | 202 | removeObserver: function(element) { // element instead of observer fixes mem leaks 203 | this.observers = this.observers.reject( function(o) { return o.element==element }); 204 | this._cacheObserverCallbacks(); 205 | }, 206 | 207 | notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' 208 | if(this[eventName+'Count'] > 0) 209 | this.observers.each( function(o) { 210 | if(o[eventName]) o[eventName](eventName, draggable, event); 211 | }); 212 | if(draggable.options[eventName]) draggable.options[eventName](draggable, event); 213 | }, 214 | 215 | _cacheObserverCallbacks: function() { 216 | ['onStart','onEnd','onDrag'].each( function(eventName) { 217 | Draggables[eventName+'Count'] = Draggables.observers.select( 218 | function(o) { return o[eventName]; } 219 | ).length; 220 | }); 221 | } 222 | }; 223 | 224 | /*--------------------------------------------------------------------------*/ 225 | 226 | var Draggable = Class.create({ 227 | initialize: function(element) { 228 | var defaults = { 229 | handle: false, 230 | reverteffect: function(element, top_offset, left_offset) { 231 | var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; 232 | new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, 233 | queue: {scope:'_draggable', position:'end'} 234 | }); 235 | }, 236 | endeffect: function(element) { 237 | var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; 238 | new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 239 | queue: {scope:'_draggable', position:'end'}, 240 | afterFinish: function(){ 241 | Draggable._dragging[element] = false 242 | } 243 | }); 244 | }, 245 | zindex: 1000, 246 | revert: false, 247 | quiet: false, 248 | scroll: false, 249 | scrollSensitivity: 20, 250 | scrollSpeed: 15, 251 | snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } 252 | delay: 0 253 | }; 254 | 255 | if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) 256 | Object.extend(defaults, { 257 | starteffect: function(element) { 258 | element._opacity = Element.getOpacity(element); 259 | Draggable._dragging[element] = true; 260 | new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 261 | } 262 | }); 263 | 264 | var options = Object.extend(defaults, arguments[1] || { }); 265 | 266 | this.element = $(element); 267 | 268 | if(options.handle && Object.isString(options.handle)) 269 | this.handle = this.element.down('.'+options.handle, 0); 270 | 271 | if(!this.handle) this.handle = $(options.handle); 272 | if(!this.handle) this.handle = this.element; 273 | 274 | if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { 275 | options.scroll = $(options.scroll); 276 | this._isScrollChild = Element.childOf(this.element, options.scroll); 277 | } 278 | 279 | Element.makePositioned(this.element); // fix IE 280 | 281 | this.options = options; 282 | this.dragging = false; 283 | 284 | this.eventMouseDown = this.initDrag.bindAsEventListener(this); 285 | Event.observe(this.handle, "mousedown", this.eventMouseDown); 286 | 287 | Draggables.register(this); 288 | }, 289 | 290 | destroy: function() { 291 | Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); 292 | Draggables.unregister(this); 293 | }, 294 | 295 | currentDelta: function() { 296 | return([ 297 | parseInt(Element.getStyle(this.element,'left') || '0'), 298 | parseInt(Element.getStyle(this.element,'top') || '0')]); 299 | }, 300 | 301 | initDrag: function(event) { 302 | if(!Object.isUndefined(Draggable._dragging[this.element]) && 303 | Draggable._dragging[this.element]) return; 304 | if(Event.isLeftClick(event)) { 305 | // abort on form elements, fixes a Firefox issue 306 | var src = Event.element(event); 307 | if((tag_name = src.tagName.toUpperCase()) && ( 308 | tag_name=='INPUT' || 309 | tag_name=='SELECT' || 310 | tag_name=='OPTION' || 311 | tag_name=='BUTTON' || 312 | tag_name=='TEXTAREA')) return; 313 | 314 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 315 | var pos = this.element.cumulativeOffset(); 316 | this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); 317 | 318 | Draggables.activate(this); 319 | Event.stop(event); 320 | } 321 | }, 322 | 323 | startDrag: function(event) { 324 | this.dragging = true; 325 | if(!this.delta) 326 | this.delta = this.currentDelta(); 327 | 328 | if(this.options.zindex) { 329 | this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); 330 | this.element.style.zIndex = this.options.zindex; 331 | } 332 | 333 | if(this.options.ghosting) { 334 | this._clone = this.element.cloneNode(true); 335 | this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); 336 | if (!this._originallyAbsolute) 337 | Position.absolutize(this.element); 338 | this.element.parentNode.insertBefore(this._clone, this.element); 339 | } 340 | 341 | if(this.options.scroll) { 342 | if (this.options.scroll == window) { 343 | var where = this._getWindowScroll(this.options.scroll); 344 | this.originalScrollLeft = where.left; 345 | this.originalScrollTop = where.top; 346 | } else { 347 | this.originalScrollLeft = this.options.scroll.scrollLeft; 348 | this.originalScrollTop = this.options.scroll.scrollTop; 349 | } 350 | } 351 | 352 | Draggables.notify('onStart', this, event); 353 | 354 | if(this.options.starteffect) this.options.starteffect(this.element); 355 | }, 356 | 357 | updateDrag: function(event, pointer) { 358 | if(!this.dragging) this.startDrag(event); 359 | 360 | if(!this.options.quiet){ 361 | Position.prepare(); 362 | Droppables.show(pointer, this.element); 363 | } 364 | 365 | Draggables.notify('onDrag', this, event); 366 | 367 | this.draw(pointer); 368 | if(this.options.change) this.options.change(this); 369 | 370 | if(this.options.scroll) { 371 | this.stopScrolling(); 372 | 373 | var p; 374 | if (this.options.scroll == window) { 375 | with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } 376 | } else { 377 | p = Position.page(this.options.scroll); 378 | p[0] += this.options.scroll.scrollLeft + Position.deltaX; 379 | p[1] += this.options.scroll.scrollTop + Position.deltaY; 380 | p.push(p[0]+this.options.scroll.offsetWidth); 381 | p.push(p[1]+this.options.scroll.offsetHeight); 382 | } 383 | var speed = [0,0]; 384 | if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); 385 | if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); 386 | if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); 387 | if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); 388 | this.startScrolling(speed); 389 | } 390 | 391 | // fix AppleWebKit rendering 392 | if(Prototype.Browser.WebKit) window.scrollBy(0,0); 393 | 394 | Event.stop(event); 395 | }, 396 | 397 | finishDrag: function(event, success) { 398 | this.dragging = false; 399 | 400 | if(this.options.quiet){ 401 | Position.prepare(); 402 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 403 | Droppables.show(pointer, this.element); 404 | } 405 | 406 | if(this.options.ghosting) { 407 | if (!this._originallyAbsolute) 408 | Position.relativize(this.element); 409 | delete this._originallyAbsolute; 410 | Element.remove(this._clone); 411 | this._clone = null; 412 | } 413 | 414 | var dropped = false; 415 | if(success) { 416 | dropped = Droppables.fire(event, this.element); 417 | if (!dropped) dropped = false; 418 | } 419 | if(dropped && this.options.onDropped) this.options.onDropped(this.element); 420 | Draggables.notify('onEnd', this, event); 421 | 422 | var revert = this.options.revert; 423 | if(revert && Object.isFunction(revert)) revert = revert(this.element); 424 | 425 | var d = this.currentDelta(); 426 | if(revert && this.options.reverteffect) { 427 | if (dropped == 0 || revert != 'failure') 428 | this.options.reverteffect(this.element, 429 | d[1]-this.delta[1], d[0]-this.delta[0]); 430 | } else { 431 | this.delta = d; 432 | } 433 | 434 | if(this.options.zindex) 435 | this.element.style.zIndex = this.originalZ; 436 | 437 | if(this.options.endeffect) 438 | this.options.endeffect(this.element); 439 | 440 | Draggables.deactivate(this); 441 | Droppables.reset(); 442 | }, 443 | 444 | keyPress: function(event) { 445 | if(event.keyCode!=Event.KEY_ESC) return; 446 | this.finishDrag(event, false); 447 | Event.stop(event); 448 | }, 449 | 450 | endDrag: function(event) { 451 | if(!this.dragging) return; 452 | this.stopScrolling(); 453 | this.finishDrag(event, true); 454 | Event.stop(event); 455 | }, 456 | 457 | draw: function(point) { 458 | var pos = this.element.cumulativeOffset(); 459 | if(this.options.ghosting) { 460 | var r = Position.realOffset(this.element); 461 | pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; 462 | } 463 | 464 | var d = this.currentDelta(); 465 | pos[0] -= d[0]; pos[1] -= d[1]; 466 | 467 | if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { 468 | pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; 469 | pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; 470 | } 471 | 472 | var p = [0,1].map(function(i){ 473 | return (point[i]-pos[i]-this.offset[i]) 474 | }.bind(this)); 475 | 476 | if(this.options.snap) { 477 | if(Object.isFunction(this.options.snap)) { 478 | p = this.options.snap(p[0],p[1],this); 479 | } else { 480 | if(Object.isArray(this.options.snap)) { 481 | p = p.map( function(v, i) { 482 | return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); 483 | } else { 484 | p = p.map( function(v) { 485 | return (v/this.options.snap).round()*this.options.snap }.bind(this)); 486 | } 487 | }} 488 | 489 | var style = this.element.style; 490 | if((!this.options.constraint) || (this.options.constraint=='horizontal')) 491 | style.left = p[0] + "px"; 492 | if((!this.options.constraint) || (this.options.constraint=='vertical')) 493 | style.top = p[1] + "px"; 494 | 495 | if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering 496 | }, 497 | 498 | stopScrolling: function() { 499 | if(this.scrollInterval) { 500 | clearInterval(this.scrollInterval); 501 | this.scrollInterval = null; 502 | Draggables._lastScrollPointer = null; 503 | } 504 | }, 505 | 506 | startScrolling: function(speed) { 507 | if(!(speed[0] || speed[1])) return; 508 | this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; 509 | this.lastScrolled = new Date(); 510 | this.scrollInterval = setInterval(this.scroll.bind(this), 10); 511 | }, 512 | 513 | scroll: function() { 514 | var current = new Date(); 515 | var delta = current - this.lastScrolled; 516 | this.lastScrolled = current; 517 | if(this.options.scroll == window) { 518 | with (this._getWindowScroll(this.options.scroll)) { 519 | if (this.scrollSpeed[0] || this.scrollSpeed[1]) { 520 | var d = delta / 1000; 521 | this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); 522 | } 523 | } 524 | } else { 525 | this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; 526 | this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; 527 | } 528 | 529 | Position.prepare(); 530 | Droppables.show(Draggables._lastPointer, this.element); 531 | Draggables.notify('onDrag', this); 532 | if (this._isScrollChild) { 533 | Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); 534 | Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; 535 | Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; 536 | if (Draggables._lastScrollPointer[0] < 0) 537 | Draggables._lastScrollPointer[0] = 0; 538 | if (Draggables._lastScrollPointer[1] < 0) 539 | Draggables._lastScrollPointer[1] = 0; 540 | this.draw(Draggables._lastScrollPointer); 541 | } 542 | 543 | if(this.options.change) this.options.change(this); 544 | }, 545 | 546 | _getWindowScroll: function(w) { 547 | var T, L, W, H; 548 | with (w.document) { 549 | if (w.document.documentElement && documentElement.scrollTop) { 550 | T = documentElement.scrollTop; 551 | L = documentElement.scrollLeft; 552 | } else if (w.document.body) { 553 | T = body.scrollTop; 554 | L = body.scrollLeft; 555 | } 556 | if (w.innerWidth) { 557 | W = w.innerWidth; 558 | H = w.innerHeight; 559 | } else if (w.document.documentElement && documentElement.clientWidth) { 560 | W = documentElement.clientWidth; 561 | H = documentElement.clientHeight; 562 | } else { 563 | W = body.offsetWidth; 564 | H = body.offsetHeight; 565 | } 566 | } 567 | return { top: T, left: L, width: W, height: H }; 568 | } 569 | }); 570 | 571 | Draggable._dragging = { }; 572 | 573 | /*--------------------------------------------------------------------------*/ 574 | 575 | var SortableObserver = Class.create({ 576 | initialize: function(element, observer) { 577 | this.element = $(element); 578 | this.observer = observer; 579 | this.lastValue = Sortable.serialize(this.element); 580 | }, 581 | 582 | onStart: function() { 583 | this.lastValue = Sortable.serialize(this.element); 584 | }, 585 | 586 | onEnd: function() { 587 | Sortable.unmark(); 588 | if(this.lastValue != Sortable.serialize(this.element)) 589 | this.observer(this.element) 590 | } 591 | }); 592 | 593 | var Sortable = { 594 | SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, 595 | 596 | sortables: { }, 597 | 598 | _findRootElement: function(element) { 599 | while (element.tagName.toUpperCase() != "BODY") { 600 | if(element.id && Sortable.sortables[element.id]) return element; 601 | element = element.parentNode; 602 | } 603 | }, 604 | 605 | options: function(element) { 606 | element = Sortable._findRootElement($(element)); 607 | if(!element) return; 608 | return Sortable.sortables[element.id]; 609 | }, 610 | 611 | destroy: function(element){ 612 | element = $(element); 613 | var s = Sortable.sortables[element.id]; 614 | 615 | if(s) { 616 | Draggables.removeObserver(s.element); 617 | s.droppables.each(function(d){ Droppables.remove(d) }); 618 | s.draggables.invoke('destroy'); 619 | 620 | delete Sortable.sortables[s.element.id]; 621 | } 622 | }, 623 | 624 | create: function(element) { 625 | element = $(element); 626 | var options = Object.extend({ 627 | element: element, 628 | tag: 'li', // assumes li children, override with tag: 'tagname' 629 | dropOnEmpty: false, 630 | tree: false, 631 | treeTag: 'ul', 632 | overlap: 'vertical', // one of 'vertical', 'horizontal' 633 | constraint: 'vertical', // one of 'vertical', 'horizontal', false 634 | containment: element, // also takes array of elements (or id's); or false 635 | handle: false, // or a CSS class 636 | only: false, 637 | delay: 0, 638 | hoverclass: null, 639 | ghosting: false, 640 | quiet: false, 641 | scroll: false, 642 | scrollSensitivity: 20, 643 | scrollSpeed: 15, 644 | format: this.SERIALIZE_RULE, 645 | 646 | // these take arrays of elements or ids and can be 647 | // used for better initialization performance 648 | elements: false, 649 | handles: false, 650 | 651 | onChange: Prototype.emptyFunction, 652 | onUpdate: Prototype.emptyFunction 653 | }, arguments[1] || { }); 654 | 655 | // clear any old sortable with same element 656 | this.destroy(element); 657 | 658 | // build options for the draggables 659 | var options_for_draggable = { 660 | revert: true, 661 | quiet: options.quiet, 662 | scroll: options.scroll, 663 | scrollSpeed: options.scrollSpeed, 664 | scrollSensitivity: options.scrollSensitivity, 665 | delay: options.delay, 666 | ghosting: options.ghosting, 667 | constraint: options.constraint, 668 | handle: options.handle }; 669 | 670 | if(options.starteffect) 671 | options_for_draggable.starteffect = options.starteffect; 672 | 673 | if(options.reverteffect) 674 | options_for_draggable.reverteffect = options.reverteffect; 675 | else 676 | if(options.ghosting) options_for_draggable.reverteffect = function(element) { 677 | element.style.top = 0; 678 | element.style.left = 0; 679 | }; 680 | 681 | if(options.endeffect) 682 | options_for_draggable.endeffect = options.endeffect; 683 | 684 | if(options.zindex) 685 | options_for_draggable.zindex = options.zindex; 686 | 687 | // build options for the droppables 688 | var options_for_droppable = { 689 | overlap: options.overlap, 690 | containment: options.containment, 691 | tree: options.tree, 692 | hoverclass: options.hoverclass, 693 | onHover: Sortable.onHover 694 | }; 695 | 696 | var options_for_tree = { 697 | onHover: Sortable.onEmptyHover, 698 | overlap: options.overlap, 699 | containment: options.containment, 700 | hoverclass: options.hoverclass 701 | }; 702 | 703 | // fix for gecko engine 704 | Element.cleanWhitespace(element); 705 | 706 | options.draggables = []; 707 | options.droppables = []; 708 | 709 | // drop on empty handling 710 | if(options.dropOnEmpty || options.tree) { 711 | Droppables.add(element, options_for_tree); 712 | options.droppables.push(element); 713 | } 714 | 715 | (options.elements || this.findElements(element, options) || []).each( function(e,i) { 716 | var handle = options.handles ? $(options.handles[i]) : 717 | (options.handle ? $(e).select('.' + options.handle)[0] : e); 718 | options.draggables.push( 719 | new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); 720 | Droppables.add(e, options_for_droppable); 721 | if(options.tree) e.treeNode = element; 722 | options.droppables.push(e); 723 | }); 724 | 725 | if(options.tree) { 726 | (Sortable.findTreeElements(element, options) || []).each( function(e) { 727 | Droppables.add(e, options_for_tree); 728 | e.treeNode = element; 729 | options.droppables.push(e); 730 | }); 731 | } 732 | 733 | // keep reference 734 | this.sortables[element.identify()] = options; 735 | 736 | // for onupdate 737 | Draggables.addObserver(new SortableObserver(element, options.onUpdate)); 738 | 739 | }, 740 | 741 | // return all suitable-for-sortable elements in a guaranteed order 742 | findElements: function(element, options) { 743 | return Element.findChildren( 744 | element, options.only, options.tree ? true : false, options.tag); 745 | }, 746 | 747 | findTreeElements: function(element, options) { 748 | return Element.findChildren( 749 | element, options.only, options.tree ? true : false, options.treeTag); 750 | }, 751 | 752 | onHover: function(element, dropon, overlap) { 753 | if(Element.isParent(dropon, element)) return; 754 | 755 | if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { 756 | return; 757 | } else if(overlap>0.5) { 758 | Sortable.mark(dropon, 'before'); 759 | if(dropon.previousSibling != element) { 760 | var oldParentNode = element.parentNode; 761 | element.style.visibility = "hidden"; // fix gecko rendering 762 | dropon.parentNode.insertBefore(element, dropon); 763 | if(dropon.parentNode!=oldParentNode) 764 | Sortable.options(oldParentNode).onChange(element); 765 | Sortable.options(dropon.parentNode).onChange(element); 766 | } 767 | } else { 768 | Sortable.mark(dropon, 'after'); 769 | var nextElement = dropon.nextSibling || null; 770 | if(nextElement != element) { 771 | var oldParentNode = element.parentNode; 772 | element.style.visibility = "hidden"; // fix gecko rendering 773 | dropon.parentNode.insertBefore(element, nextElement); 774 | if(dropon.parentNode!=oldParentNode) 775 | Sortable.options(oldParentNode).onChange(element); 776 | Sortable.options(dropon.parentNode).onChange(element); 777 | } 778 | } 779 | }, 780 | 781 | onEmptyHover: function(element, dropon, overlap) { 782 | var oldParentNode = element.parentNode; 783 | var droponOptions = Sortable.options(dropon); 784 | 785 | if(!Element.isParent(dropon, element)) { 786 | var index; 787 | 788 | var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); 789 | var child = null; 790 | 791 | if(children) { 792 | var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); 793 | 794 | for (index = 0; index < children.length; index += 1) { 795 | if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { 796 | offset -= Element.offsetSize (children[index], droponOptions.overlap); 797 | } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { 798 | child = index + 1 < children.length ? children[index + 1] : null; 799 | break; 800 | } else { 801 | child = children[index]; 802 | break; 803 | } 804 | } 805 | } 806 | 807 | dropon.insertBefore(element, child); 808 | 809 | Sortable.options(oldParentNode).onChange(element); 810 | droponOptions.onChange(element); 811 | } 812 | }, 813 | 814 | unmark: function() { 815 | if(Sortable._marker) Sortable._marker.hide(); 816 | }, 817 | 818 | mark: function(dropon, position) { 819 | // mark on ghosting only 820 | var sortable = Sortable.options(dropon.parentNode); 821 | if(sortable && !sortable.ghosting) return; 822 | 823 | if(!Sortable._marker) { 824 | Sortable._marker = 825 | ($('dropmarker') || Element.extend(document.createElement('DIV'))). 826 | hide().addClassName('dropmarker').setStyle({position:'absolute'}); 827 | document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); 828 | } 829 | var offsets = dropon.cumulativeOffset(); 830 | Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); 831 | 832 | if(position=='after') 833 | if(sortable.overlap == 'horizontal') 834 | Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); 835 | else 836 | Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); 837 | 838 | Sortable._marker.show(); 839 | }, 840 | 841 | _tree: function(element, options, parent) { 842 | var children = Sortable.findElements(element, options) || []; 843 | 844 | for (var i = 0; i < children.length; ++i) { 845 | var match = children[i].id.match(options.format); 846 | 847 | if (!match) continue; 848 | 849 | var child = { 850 | id: encodeURIComponent(match ? match[1] : null), 851 | element: element, 852 | parent: parent, 853 | children: [], 854 | position: parent.children.length, 855 | container: $(children[i]).down(options.treeTag) 856 | }; 857 | 858 | /* Get the element containing the children and recurse over it */ 859 | if (child.container) 860 | this._tree(child.container, options, child); 861 | 862 | parent.children.push (child); 863 | } 864 | 865 | return parent; 866 | }, 867 | 868 | tree: function(element) { 869 | element = $(element); 870 | var sortableOptions = this.options(element); 871 | var options = Object.extend({ 872 | tag: sortableOptions.tag, 873 | treeTag: sortableOptions.treeTag, 874 | only: sortableOptions.only, 875 | name: element.id, 876 | format: sortableOptions.format 877 | }, arguments[1] || { }); 878 | 879 | var root = { 880 | id: null, 881 | parent: null, 882 | children: [], 883 | container: element, 884 | position: 0 885 | }; 886 | 887 | return Sortable._tree(element, options, root); 888 | }, 889 | 890 | /* Construct a [i] index for a particular node */ 891 | _constructIndex: function(node) { 892 | var index = ''; 893 | do { 894 | if (node.id) index = '[' + node.position + ']' + index; 895 | } while ((node = node.parent) != null); 896 | return index; 897 | }, 898 | 899 | sequence: function(element) { 900 | element = $(element); 901 | var options = Object.extend(this.options(element), arguments[1] || { }); 902 | 903 | return $(this.findElements(element, options) || []).map( function(item) { 904 | return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; 905 | }); 906 | }, 907 | 908 | setSequence: function(element, new_sequence) { 909 | element = $(element); 910 | var options = Object.extend(this.options(element), arguments[2] || { }); 911 | 912 | var nodeMap = { }; 913 | this.findElements(element, options).each( function(n) { 914 | if (n.id.match(options.format)) 915 | nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; 916 | n.parentNode.removeChild(n); 917 | }); 918 | 919 | new_sequence.each(function(ident) { 920 | var n = nodeMap[ident]; 921 | if (n) { 922 | n[1].appendChild(n[0]); 923 | delete nodeMap[ident]; 924 | } 925 | }); 926 | }, 927 | 928 | serialize: function(element) { 929 | element = $(element); 930 | var options = Object.extend(Sortable.options(element), arguments[1] || { }); 931 | var name = encodeURIComponent( 932 | (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); 933 | 934 | if (options.tree) { 935 | return Sortable.tree(element, arguments[1]).children.map( function (item) { 936 | return [name + Sortable._constructIndex(item) + "[id]=" + 937 | encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); 938 | }).flatten().join('&'); 939 | } else { 940 | return Sortable.sequence(element, arguments[1]).map( function(item) { 941 | return name + "[]=" + encodeURIComponent(item); 942 | }).join('&'); 943 | } 944 | } 945 | }; 946 | 947 | // Returns true if child is contained within element 948 | Element.isParent = function(child, element) { 949 | if (!child.parentNode || child == element) return false; 950 | if (child.parentNode == element) return true; 951 | return Element.isParent(child.parentNode, element); 952 | }; 953 | 954 | Element.findChildren = function(element, only, recursive, tagName) { 955 | if(!element.hasChildNodes()) return null; 956 | tagName = tagName.toUpperCase(); 957 | if(only) only = [only].flatten(); 958 | var elements = []; 959 | $A(element.childNodes).each( function(e) { 960 | if(e.tagName && e.tagName.toUpperCase()==tagName && 961 | (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) 962 | elements.push(e); 963 | if(recursive) { 964 | var grandchildren = Element.findChildren(e, only, recursive, tagName); 965 | if(grandchildren) elements.push(grandchildren); 966 | } 967 | }); 968 | 969 | return (elements.length>0 ? elements.flatten() : []); 970 | }; 971 | 972 | Element.offsetSize = function (element, type) { 973 | return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; 974 | }; --------------------------------------------------------------------------------