├── lib ├── chartkick │ ├── version.rb │ ├── sinatra.rb │ ├── rails.rb │ ├── engine.rb │ └── helper.rb └── chartkick.rb ├── Gemfile ├── test ├── test_helper.rb └── chartkick_test.rb ├── Rakefile ├── .gitignore ├── chartkick.gemspec ├── LICENSE.txt ├── CHANGELOG.md ├── README.md └── app └── assets └── javascripts └── chartkick.js /lib/chartkick/version.rb: -------------------------------------------------------------------------------- 1 | module Chartkick 2 | VERSION = "2.2.5" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in chartkick.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/chartkick/sinatra.rb: -------------------------------------------------------------------------------- 1 | require "sinatra/base" 2 | 3 | class Sinatra::Base 4 | helpers Chartkick::Helper 5 | end 6 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | Bundler.require(:default) 3 | require "minitest/autorun" 4 | require "minitest/pride" 5 | -------------------------------------------------------------------------------- /lib/chartkick/rails.rb: -------------------------------------------------------------------------------- 1 | if Rails.version >= "3.1" 2 | require "chartkick/engine" 3 | else 4 | ActionView::Base.send :include, Chartkick::Helper 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | task default: :test 5 | Rake::TestTask.new do |t| 6 | t.libs << "test" 7 | t.pattern = "test/**/*_test.rb" 8 | end 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /test/chartkick_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class TestChartkick < Minitest::Test 4 | include Chartkick::Helper 5 | 6 | # TODO actual tests 7 | 8 | def setup 9 | @data = [[34, 42], [56, 49]] 10 | end 11 | 12 | def test_line_chart 13 | assert line_chart(@data) 14 | end 15 | 16 | def test_pie_chart 17 | assert pie_chart(@data) 18 | end 19 | 20 | def test_column_chart 21 | assert column_chart(@data) 22 | end 23 | 24 | def test_options_not_mutated 25 | options = {id: "boom"} 26 | line_chart @data, options 27 | assert_equal "boom", options[:id] 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/chartkick/engine.rb: -------------------------------------------------------------------------------- 1 | module Chartkick 2 | class Engine < ::Rails::Engine 3 | initializer "precompile", group: :all do |app| 4 | if app.config.respond_to?(:assets) 5 | if defined?(Sprockets) && Gem::Version.new(Sprockets::VERSION) >= Gem::Version.new("4.0.0.beta1") 6 | app.config.assets.precompile << "chartkick.js" 7 | else 8 | # use a proc instead of a string 9 | app.config.assets.precompile << proc { |path| path == "chartkick.js" } 10 | end 11 | end 12 | end 13 | 14 | initializer "helper" do 15 | ActiveSupport.on_load(:action_view) do 16 | include Helper 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/chartkick.rb: -------------------------------------------------------------------------------- 1 | require "chartkick/version" 2 | require "chartkick/helper" 3 | require "chartkick/rails" if defined?(Rails) 4 | require "chartkick/sinatra" if defined?(Sinatra) 5 | 6 | module Chartkick 7 | class << self 8 | attr_accessor :content_for 9 | attr_accessor :options 10 | end 11 | self.options = {} 12 | end 13 | 14 | # for multiple series 15 | # use Enumerable so it can be called on arrays 16 | module Enumerable 17 | def chart_json 18 | if is_a?(Hash) && (key = keys.first) && key.is_a?(Array) && key.size == 2 19 | group_by { |k, _v| k[0] }.map do |name, data| 20 | {name: name, data: data.map { |k, v| [k[1], v] }} 21 | end 22 | else 23 | self 24 | end.to_json 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /chartkick.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "chartkick/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "chartkick" 8 | spec.version = Chartkick::VERSION 9 | spec.authors = ["Andrew Kane"] 10 | spec.email = ["andrew@chartkick.com"] 11 | spec.description = "Create beautiful JavaScript charts with one line of Ruby" 12 | spec.summary = "Create beautiful JavaScript charts with one line of Ruby" 13 | spec.homepage = "http://chartkick.com" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.3" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "minitest" 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Andrew Kane 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/chartkick/helper.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "erb" 3 | 4 | module Chartkick 5 | module Helper 6 | def line_chart(data_source, options = {}) 7 | chartkick_chart "LineChart", data_source, options 8 | end 9 | 10 | def pie_chart(data_source, options = {}) 11 | chartkick_chart "PieChart", data_source, options 12 | end 13 | 14 | def column_chart(data_source, options = {}) 15 | chartkick_chart "ColumnChart", data_source, options 16 | end 17 | 18 | def bar_chart(data_source, options = {}) 19 | chartkick_chart "BarChart", data_source, options 20 | end 21 | 22 | def area_chart(data_source, options = {}) 23 | chartkick_chart "AreaChart", data_source, options 24 | end 25 | 26 | def scatter_chart(data_source, options = {}) 27 | chartkick_chart "ScatterChart", data_source, options 28 | end 29 | 30 | def geo_chart(data_source, options = {}) 31 | chartkick_chart "GeoChart", data_source, options 32 | end 33 | 34 | def timeline(data_source, options = {}) 35 | chartkick_chart "Timeline", data_source, options 36 | end 37 | 38 | private 39 | 40 | def chartkick_chart(klass, data_source, options) 41 | @chartkick_chart_id ||= 0 42 | options = chartkick_deep_merge(Chartkick.options, options) 43 | element_id = options.delete(:id) || "chart-#{@chartkick_chart_id += 1}" 44 | height = options.delete(:height) || "300px" 45 | width = options.delete(:width) || "100%" 46 | defer = !!options.delete(:defer) 47 | # content_for: nil must override default 48 | content_for = options.key?(:content_for) ? options.delete(:content_for) : Chartkick.content_for 49 | nonce = options.key?(:nonce) ? " nonce=\"#{ERB::Util.html_escape(options.delete(:nonce))}\"" : nil 50 | html = (options.delete(:html) || %(
Loading...
)) % {id: ERB::Util.html_escape(element_id), height: ERB::Util.html_escape(height), width: ERB::Util.html_escape(width)} 51 | 52 | createjs = "new Chartkick.#{klass}(#{element_id.to_json}, #{data_source.respond_to?(:chart_json) ? data_source.chart_json : data_source.to_json}, #{options.to_json});" 53 | if defer 54 | js = < 56 | (function() { 57 | var createChart = function() { #{createjs} }; 58 | if (window.addEventListener) { 59 | window.addEventListener("load", createChart, true); 60 | } else if (window.attachEvent) { 61 | window.attachEvent("onload", createChart); 62 | } else { 63 | createChart(); 64 | } 65 | })(); 66 | 67 | JS 68 | else 69 | js = < 71 | #{createjs} 72 | 73 | JS 74 | end 75 | 76 | if content_for 77 | content_for(content_for) { js.respond_to?(:html_safe) ? js.html_safe : js } 78 | else 79 | html += js 80 | end 81 | 82 | html.respond_to?(:html_safe) ? html.html_safe : html 83 | end 84 | 85 | # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/hash/deep_merge.rb 86 | def chartkick_deep_merge(hash_a, hash_b) 87 | hash_a = hash_a.dup 88 | hash_b.each_pair do |k, v| 89 | tv = hash_a[k] 90 | hash_a[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v 91 | end 92 | hash_a 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.2.5 2 | 3 | - Updated Chart.js to 2.7.1 4 | 5 | ## 2.2.4 6 | 7 | - Added compatibility with Rails API 8 | - Updated Chartkick.js to 2.2.4 9 | 10 | ## 2.2.3 11 | 12 | - Updated Chartkick.js to 2.2.3 13 | - Updated Chart.js to 2.5.0 14 | 15 | ## 2.2.2 16 | 17 | - Updated Chartkick.js to 2.2.2 18 | 19 | ## 2.2.1 20 | 21 | - Updated Chartkick.js to 2.2.1 22 | 23 | ## 2.2.0 24 | 25 | - Updated Chartkick.js to 2.2.0 26 | - Improved JavaScript API 27 | - Added `download` option - *Chart.js only* 28 | - Added `refresh` option 29 | - Added `donut` option to pie chart 30 | 31 | ## 2.1.3 32 | 33 | - Updated Chartkick.js to 2.1.2 - fixes missing zero values for Chart.js 34 | 35 | ## 2.1.2 36 | 37 | - Added `defer` option 38 | - Added `nonce` option 39 | - Updated Chartkick.js to 2.1.1 40 | 41 | ## 2.1.1 42 | 43 | - Use custom version of Chart.js to fix label overlap 44 | 45 | ## 2.1.0 46 | 47 | - Added basic support for new Google Charts loader 48 | - Added `configure` function 49 | - Dropped jQuery and Zepto dependencies for AJAX 50 | - Updated Chart.js to 2.2.2 51 | 52 | ## 2.0.2 53 | 54 | - Updated Chartkick.js to 2.0.1 55 | - Updated Chart.js to 2.2.1 56 | 57 | ## 2.0.1 58 | 59 | - Small Chartkick.js fixes 60 | - Updated Chart.js to 2.2.0 61 | 62 | ## 2.0.0 63 | 64 | - Chart.js is now the default adapter - yay open source! 65 | - Axis types are automatically detected - no need for `discrete: true` 66 | - Better date support 67 | - New JavaScript API 68 | 69 | ## 1.5.2 70 | 71 | - Fixed Sprockets error 72 | 73 | ## 1.5.1 74 | 75 | - Updated chartkick.js to latest version 76 | - Included `Chart.bundle.js` 77 | 78 | ## 1.5.0 79 | 80 | - Added Chart.js adapter **beta** 81 | - Fixed line height on timeline charts 82 | 83 | ## 1.4.2 84 | 85 | - Added `width` option 86 | - Added `label` option 87 | - Added support for `stacked: false` for area charts 88 | - Lazy load adapters 89 | - Better tooltip for dates for Google Charts 90 | - Fixed asset precompilation issue with Rails 5 91 | 92 | ## 1.4.1 93 | 94 | - Fixed regression with `min: nil` 95 | 96 | ## 1.4.0 97 | 98 | - Added scatter chart 99 | - Added axis titles 100 | 101 | ## 1.3.2 102 | 103 | - Fixed `except` error when not using Rails 104 | 105 | ## 1.3.1 106 | 107 | - Fixed blank screen bug 108 | - Fixed language support 109 | 110 | ## 1.3.0 111 | 112 | - Added timelines 113 | 114 | ## 1.2.5 115 | 116 | - Added support for multiple groups 117 | - Added `html` option 118 | 119 | ## 1.2.4 120 | 121 | - Added global options 122 | - Added `colors` option 123 | 124 | ## 1.2.3 125 | 126 | - Added geo chart 127 | - Added `discrete` option 128 | 129 | ## 1.2.2 130 | 131 | - Added global `content_for` option 132 | - Added `stacked` option 133 | 134 | ## 1.2.1 135 | 136 | - Added localization for Google Charts 137 | 138 | ## 1.2.0 139 | 140 | - Added bar chart and area chart 141 | - Resize Google Charts on window resize 142 | 143 | ## 1.1.3 144 | 145 | - Added content_for option 146 | 147 | ## 1.1.2 148 | 149 | - Updated chartkick.js to v1.0.1 150 | 151 | ## 1.1.1 152 | 153 | - Added support for Sinatra 154 | 155 | ## 1.1.0 156 | 157 | - Added support for Padrino and Rails 2.3+ 158 | 159 | ## 1.0.1 160 | 161 | - Updated chartkick.js to v1.0.1 162 | 163 | ## 1.0.0 164 | 165 | - Use semantic versioning (no changes) 166 | 167 | ## 0.0.5 168 | 169 | - Removed `:min => 0` default for charts with negative values 170 | - Show legend when data given in `{:name => "", :data => {}}` format 171 | 172 | ## 0.0.4 173 | 174 | - Fix for `Uncaught ReferenceError: Chartkick is not defined` when chartkick.js is included in the `` 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chartkick 2 | 3 | Create beautiful JavaScript charts with one line of Ruby. No more fighting with charting libraries! 4 | 5 | [See it in action](https://www.chartkick.com) 6 | 7 | :fire: For admin charts and dashboards, check out [Blazer](https://github.com/ankane/blazer/) 8 | 9 | :two_hearts: A perfect companion to [Groupdate](https://github.com/ankane/groupdate), [Hightop](https://github.com/ankane/hightop), and [ActiveMedian](https://github.com/ankane/active_median) 10 | 11 | ## Charts 12 | 13 | Line chart 14 | 15 | ```erb 16 | <%= line_chart User.group_by_day(:created_at).count %> 17 | ``` 18 | 19 | Pie chart 20 | 21 | ```erb 22 | <%= pie_chart Goal.group(:name).count %> 23 | ``` 24 | 25 | Column chart 26 | 27 | ```erb 28 | <%= column_chart Task.group_by_hour_of_day(:created_at, format: "%l %P").count %> 29 | ``` 30 | 31 | Bar chart 32 | 33 | ```erb 34 | <%= bar_chart Shirt.group(:size).sum(:price) %> 35 | ``` 36 | 37 | Area chart 38 | 39 | ```erb 40 | <%= area_chart Visit.group_by_minute(:created_at).maximum(:load_time) %> 41 | ``` 42 | 43 | Scatter chart 44 | 45 | ```erb 46 | <%= scatter_chart City.pluck(:size, :population) %> 47 | ``` 48 | 49 | Geo chart - *Google Charts* 50 | 51 | ```erb 52 | <%= geo_chart Medal.group(:country).count %> 53 | ``` 54 | 55 | Timeline - *Google Charts* 56 | 57 | ```erb 58 | <%= timeline [ 59 | ["Washington", "1789-04-29", "1797-03-03"], 60 | ["Adams", "1797-03-03", "1801-03-03"], 61 | ["Jefferson", "1801-03-03", "1809-03-03"] 62 | ] %> 63 | ``` 64 | 65 | Multiple series 66 | 67 | ```erb 68 | <%= line_chart @goals.map { |goal| 69 | {name: goal.name, data: goal.feats.group_by_week(:created_at).count} 70 | } %> 71 | ``` 72 | 73 | or 74 | 75 | ```erb 76 | <%= line_chart Feat.group(:goal_id).group_by_week(:created_at).count %> 77 | ``` 78 | 79 | ### Say Goodbye To Timeouts 80 | 81 | Make your pages load super fast and stop worrying about timeouts. Give each chart its own endpoint. 82 | 83 | ```erb 84 | <%= line_chart completed_tasks_charts_path %> 85 | ``` 86 | 87 | And in your controller, pass the data as JSON. 88 | 89 | ```ruby 90 | class ChartsController < ApplicationController 91 | def completed_tasks 92 | render json: Task.group_by_day(:completed_at).count 93 | end 94 | end 95 | ``` 96 | 97 | For multiple series, add `chart_json` at the end. 98 | 99 | ```ruby 100 | render json: Task.group(:goal_id).group_by_day(:completed_at).count.chart_json 101 | ``` 102 | 103 | ### Options 104 | 105 | Id, width, and height 106 | 107 | ```erb 108 | <%= line_chart data, id: "users-chart", width: "800px", height: "500px" %> 109 | ``` 110 | 111 | Min and max values 112 | 113 | ```erb 114 | <%= line_chart data, min: 1000, max: 5000 %> 115 | ``` 116 | 117 | `min` defaults to 0 for charts with non-negative values. Use `nil` to let the charting library decide. 118 | 119 | Colors 120 | 121 | ```erb 122 | <%= line_chart data, colors: ["#b00", "#666"] %> 123 | ``` 124 | 125 | Stacked columns or bars 126 | 127 | ```erb 128 | <%= column_chart data, stacked: true %> 129 | ``` 130 | 131 | Discrete axis 132 | 133 | ```erb 134 | <%= line_chart data, discrete: true %> 135 | ``` 136 | 137 | Label (for single series) 138 | 139 | ```erb 140 | <%= line_chart data, label: "Value" %> 141 | ``` 142 | 143 | Axis titles 144 | 145 | ```erb 146 | <%= line_chart data, xtitle: "Time", ytitle: "Population" %> 147 | ``` 148 | 149 | Straight lines between points instead of a curve 150 | 151 | ```erb 152 | <%= line_chart data, curve: false %> 153 | ``` 154 | 155 | Hide points 156 | 157 | ```erb 158 | <%= line_chart data, points: false %> 159 | ``` 160 | 161 | Show or hide legend 162 | 163 | ```erb 164 | <%= line_chart data, legend: false %> 165 | ``` 166 | 167 | Specify legend position 168 | 169 | ```erb 170 | <%= line_chart data, legend: "bottom" %> 171 | ``` 172 | 173 | Donut chart 174 | 175 | ```erb 176 | <%= pie_chart data, donut: true %> 177 | ``` 178 | 179 | Refresh data from a remote source every `n` seconds 180 | 181 | ```erb 182 | <%= line_chart url, refresh: 60 %> 183 | ``` 184 | 185 | You can pass options directly to the charting library with: 186 | 187 | ```erb 188 | <%= line_chart data, library: {backgroundColor: "#eee"} %> 189 | ``` 190 | 191 | See the documentation for [Chart.js](http://www.chartjs.org/docs/), [Google Charts](https://developers.google.com/chart/interactive/docs/gallery), and [Highcharts](http://api.highcharts.com/highcharts) for more info. 192 | 193 | ### Global Options 194 | 195 | To set options for all of your charts, create an initializer `config/initializers/chartkick.rb` with: 196 | 197 | ```ruby 198 | Chartkick.options = { 199 | height: "400px", 200 | colors: ["#b00", "#666"] 201 | } 202 | ``` 203 | 204 | Customize the html 205 | 206 | ```ruby 207 | Chartkick.options[:html] = '
Loading...
' 208 | ``` 209 | 210 | You capture the JavaScript in a content block with: 211 | 212 | ```ruby 213 | Chartkick.options[:content_for] = :charts_js 214 | ``` 215 | 216 | Then, in your layout: 217 | 218 | ```erb 219 | <%= yield :charts_js %> 220 | <%= yield_content :charts_js %> 221 | ``` 222 | 223 | This is great for including all of your JavaScript at the bottom of the page. 224 | 225 | ### Data 226 | 227 | Pass data as a Hash or Array 228 | 229 | ```erb 230 | <%= pie_chart({"Football" => 10, "Basketball" => 5}) %> 231 | <%= pie_chart [["Football", 10], ["Basketball", 5]] %> 232 | ``` 233 | 234 | For multiple series, use the format 235 | 236 | ```erb 237 | <%= line_chart [ 238 | {name: "Series A", data: series_a}, 239 | {name: "Series B", data: series_b} 240 | ] %> 241 | ``` 242 | 243 | Times can be a time, a timestamp, or a string (strings are parsed) 244 | 245 | ```erb 246 | <%= line_chart({20.day.ago => 5, 1368174456 => 4, "2013-05-07 00:00:00 UTC" => 7}) %> 247 | ``` 248 | 249 | ### Download Charts 250 | 251 | *Chart.js only* 252 | 253 | Give users the ability to download charts. It all happens in the browser - no server-side code needed. 254 | 255 | ```erb 256 | <%= line_chart data, download: true %> 257 | ``` 258 | 259 | Set the filename 260 | 261 | ```erb 262 | <%= line_chart data, download: "boom" %> 263 | ``` 264 | 265 | **Note:** Safari will open the image in a new window instead of downloading. 266 | 267 | ## Installation 268 | 269 | Add this line to your application's Gemfile: 270 | 271 | ```ruby 272 | gem "chartkick" 273 | ``` 274 | 275 | Next, choose your charting library. 276 | 277 | **Note:** In the instructions below, `application.js` must be included **before** the helper methods in your views, unless using the `:content_for` option. 278 | 279 | #### Chart.js 280 | 281 | In `application.js`, add: 282 | 283 | ```js 284 | //= require Chart.bundle 285 | //= require chartkick 286 | ``` 287 | 288 | #### Google Charts 289 | 290 | In `application.js`, add: 291 | 292 | ```js 293 | //= require chartkick 294 | ``` 295 | 296 | In your views, before `application.js`, add: 297 | 298 | ```erb 299 | <%= javascript_include_tag "https://www.gstatic.com/charts/loader.js" %> 300 | ``` 301 | 302 | #### Highcharts 303 | 304 | Download [highcharts.js](https://code.highcharts.com/highcharts.js) into `vendor/assets/javascripts`. 305 | 306 | In `application.js`, add: 307 | 308 | ```js 309 | //= require highcharts 310 | //= require chartkick 311 | ``` 312 | 313 | Works with Highcharts 2.1+ 314 | 315 | ### Sinatra and Padrino 316 | 317 | You must include `chartkick.js` manually. [Download it here](https://raw.github.com/ankane/chartkick/master/app/assets/javascripts/chartkick.js) 318 | 319 | ```html 320 | 321 | ``` 322 | 323 | ### Localization 324 | 325 | To specify a language for Google Charts, add: 326 | 327 | ```javascript 328 | Chartkick.configure({language: "de"}); 329 | ``` 330 | 331 | after the JavaScript files and before your charts. 332 | 333 | ### Multiple Libraries 334 | 335 | If more than one charting library is loaded, choose between them with: 336 | 337 | ```erb 338 | <%= line_chart data, adapter: "google" %> 339 | ``` 340 | 341 | ## JavaScript API 342 | 343 | Access a chart with: 344 | 345 | ```javascript 346 | var chart = Chartkick.charts["chart-id"] 347 | ``` 348 | 349 | Get the underlying chart object with: 350 | 351 | ```javascript 352 | chart.getChartObject() 353 | ``` 354 | 355 | You can also use: 356 | 357 | ```javascript 358 | chart.getElement() 359 | chart.getData() 360 | chart.getOptions() 361 | chart.getAdapter() 362 | ``` 363 | 364 | Update the data with: 365 | 366 | ```javascript 367 | chart.updateData(newData) 368 | ``` 369 | 370 | You can also specify new options: 371 | 372 | ```javascript 373 | chart.setOptions(newOptions) 374 | // or 375 | chart.updateData(newData, newOptions) 376 | ``` 377 | 378 | Refresh the data from a remote source: 379 | 380 | ```javascript 381 | chart.refreshData() 382 | ``` 383 | 384 | Redraw the chart with: 385 | 386 | ```javascript 387 | chart.redraw() 388 | ``` 389 | 390 | Loop over charts with: 391 | 392 | ```javascript 393 | Chartkick.eachChart( function(chart) { 394 | // do something 395 | }) 396 | ``` 397 | 398 | ## No Ruby? No Problem 399 | 400 | Check out [chartkick.js](https://github.com/ankane/chartkick.js) 401 | 402 | ## Tutorials 403 | 404 | - [Charts with Chartkick and Groupdate](https://gorails.com/episodes/charts-with-chartkick-and-groupdate) 405 | - [Make Easy Graphs and Charts on Rails with Chartkick](https://www.sitepoint.com/make-easy-graphs-and-charts-on-rails-with-chartkick/) 406 | - [Practical Graphs on Rails: Chartkick in Practice](https://www.sitepoint.com/graphs-on-rails-chartkick-in-practice/) 407 | 408 | ## Upgrading 409 | 410 | ### 2.0 411 | 412 | Breaking changes 413 | 414 | - Chart.js is now the default adapter if multiple are loaded - yay open source! 415 | - Axis types are automatically detected - no need for `discrete: true` 416 | - Better date support - dates are no longer treated as UTC 417 | 418 | ## Credits 419 | 420 | Chartkick uses [iso8601.js](https://github.com/Do/iso8601.js) to parse dates and times. 421 | 422 | ## History 423 | 424 | View the [changelog](https://github.com/ankane/chartkick/blob/master/CHANGELOG.md) 425 | 426 | Chartkick follows [Semantic Versioning](http://semver.org/) 427 | 428 | ## Contributing 429 | 430 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 431 | 432 | - [Report bugs](https://github.com/ankane/chartkick/issues) 433 | - Fix bugs and [submit pull requests](https://github.com/ankane/chartkick/pulls) 434 | - Write, clarify, or fix documentation 435 | - Suggest or add new features 436 | -------------------------------------------------------------------------------- /app/assets/javascripts/chartkick.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Chartkick.js 3 | * Create beautiful charts with one line of JavaScript 4 | * https://github.com/ankane/chartkick.js 5 | * v2.2.4 6 | * MIT License 7 | */ 8 | 9 | /*jslint browser: true, indent: 2, plusplus: true, vars: true */ 10 | 11 | (function (window) { 12 | 'use strict'; 13 | 14 | var config = window.Chartkick || {}; 15 | var Chartkick, ISO8601_PATTERN, DECIMAL_SEPARATOR, adapters = []; 16 | var DATE_PATTERN = /^(\d\d\d\d)(\-)?(\d\d)(\-)?(\d\d)$/i; 17 | var GoogleChartsAdapter, HighchartsAdapter, ChartjsAdapter; 18 | var pendingRequests = [], runningRequests = 0, maxRequests = 4; 19 | 20 | // helpers 21 | 22 | function isArray(variable) { 23 | return Object.prototype.toString.call(variable) === "[object Array]"; 24 | } 25 | 26 | function isFunction(variable) { 27 | return variable instanceof Function; 28 | } 29 | 30 | function isPlainObject(variable) { 31 | return !isFunction(variable) && variable instanceof Object; 32 | } 33 | 34 | // https://github.com/madrobby/zepto/blob/master/src/zepto.js 35 | function extend(target, source) { 36 | var key; 37 | for (key in source) { 38 | if (isPlainObject(source[key]) || isArray(source[key])) { 39 | if (isPlainObject(source[key]) && !isPlainObject(target[key])) { 40 | target[key] = {}; 41 | } 42 | if (isArray(source[key]) && !isArray(target[key])) { 43 | target[key] = []; 44 | } 45 | extend(target[key], source[key]); 46 | } else if (source[key] !== undefined) { 47 | target[key] = source[key]; 48 | } 49 | } 50 | } 51 | 52 | function merge(obj1, obj2) { 53 | var target = {}; 54 | extend(target, obj1); 55 | extend(target, obj2); 56 | return target; 57 | } 58 | 59 | // https://github.com/Do/iso8601.js 60 | ISO8601_PATTERN = /(\d\d\d\d)(\-)?(\d\d)(\-)?(\d\d)(T)?(\d\d)(:)?(\d\d)?(:)?(\d\d)?([\.,]\d+)?($|Z|([\+\-])(\d\d)(:)?(\d\d)?)/i; 61 | DECIMAL_SEPARATOR = String(1.5).charAt(1); 62 | 63 | function parseISO8601(input) { 64 | var day, hour, matches, milliseconds, minutes, month, offset, result, seconds, type, year; 65 | type = Object.prototype.toString.call(input); 66 | if (type === "[object Date]") { 67 | return input; 68 | } 69 | if (type !== "[object String]") { 70 | return; 71 | } 72 | matches = input.match(ISO8601_PATTERN); 73 | if (matches) { 74 | year = parseInt(matches[1], 10); 75 | month = parseInt(matches[3], 10) - 1; 76 | day = parseInt(matches[5], 10); 77 | hour = parseInt(matches[7], 10); 78 | minutes = matches[9] ? parseInt(matches[9], 10) : 0; 79 | seconds = matches[11] ? parseInt(matches[11], 10) : 0; 80 | milliseconds = matches[12] ? parseFloat(DECIMAL_SEPARATOR + matches[12].slice(1)) * 1000 : 0; 81 | result = Date.UTC(year, month, day, hour, minutes, seconds, milliseconds); 82 | if (matches[13] && matches[14]) { 83 | offset = matches[15] * 60; 84 | if (matches[17]) { 85 | offset += parseInt(matches[17], 10); 86 | } 87 | offset *= matches[14] === "-" ? -1 : 1; 88 | result -= offset * 60 * 1000; 89 | } 90 | return new Date(result); 91 | } 92 | } 93 | // end iso8601.js 94 | 95 | function negativeValues(series) { 96 | var i, j, data; 97 | for (i = 0; i < series.length; i++) { 98 | data = series[i].data; 99 | for (j = 0; j < data.length; j++) { 100 | if (data[j][1] < 0) { 101 | return true; 102 | } 103 | } 104 | } 105 | return false; 106 | } 107 | 108 | function jsOptionsFunc(defaultOptions, hideLegend, setTitle, setMin, setMax, setStacked, setXtitle, setYtitle) { 109 | return function (chart, opts, chartOptions) { 110 | var series = chart.data; 111 | var options = merge({}, defaultOptions); 112 | options = merge(options, chartOptions || {}); 113 | 114 | if (chart.hideLegend || "legend" in opts) { 115 | hideLegend(options, opts.legend, chart.hideLegend); 116 | } 117 | 118 | if (opts.title) { 119 | setTitle(options, opts.title); 120 | } 121 | 122 | // min 123 | if ("min" in opts) { 124 | setMin(options, opts.min); 125 | } else if (!negativeValues(series)) { 126 | setMin(options, 0); 127 | } 128 | 129 | // max 130 | if (opts.max) { 131 | setMax(options, opts.max); 132 | } 133 | 134 | if ("stacked" in opts) { 135 | setStacked(options, opts.stacked); 136 | } 137 | 138 | if (opts.colors) { 139 | options.colors = opts.colors; 140 | } 141 | 142 | if (opts.xtitle) { 143 | setXtitle(options, opts.xtitle); 144 | } 145 | 146 | if (opts.ytitle) { 147 | setYtitle(options, opts.ytitle); 148 | } 149 | 150 | // merge library last 151 | options = merge(options, opts.library || {}); 152 | 153 | return options; 154 | }; 155 | } 156 | 157 | function setText(element, text) { 158 | if (document.body.innerText) { 159 | element.innerText = text; 160 | } else { 161 | element.textContent = text; 162 | } 163 | } 164 | 165 | function chartError(element, message) { 166 | setText(element, "Error Loading Chart: " + message); 167 | element.style.color = "#ff0000"; 168 | } 169 | 170 | function pushRequest(element, url, success) { 171 | pendingRequests.push([element, url, success]); 172 | runNext(); 173 | } 174 | 175 | function runNext() { 176 | if (runningRequests < maxRequests) { 177 | var request = pendingRequests.shift() 178 | if (request) { 179 | runningRequests++; 180 | getJSON(request[0], request[1], request[2]); 181 | runNext(); 182 | } 183 | } 184 | } 185 | 186 | function requestComplete() { 187 | runningRequests--; 188 | runNext(); 189 | } 190 | 191 | function getJSON(element, url, success) { 192 | ajaxCall(url, success, function (jqXHR, textStatus, errorThrown) { 193 | var message = (typeof errorThrown === "string") ? errorThrown : errorThrown.message; 194 | chartError(element, message); 195 | }); 196 | } 197 | 198 | function ajaxCall(url, success, error) { 199 | var $ = window.jQuery || window.Zepto || window.$; 200 | 201 | if ($) { 202 | $.ajax({ 203 | dataType: "json", 204 | url: url, 205 | success: success, 206 | error: error, 207 | complete: requestComplete 208 | }); 209 | } else { 210 | var xhr = new XMLHttpRequest(); 211 | xhr.open("GET", url, true); 212 | xhr.setRequestHeader("Content-Type", "application/json"); 213 | xhr.onload = function () { 214 | requestComplete(); 215 | if (xhr.status === 200) { 216 | success(JSON.parse(xhr.responseText), xhr.statusText, xhr); 217 | } else { 218 | error(xhr, "error", xhr.statusText); 219 | } 220 | }; 221 | xhr.send(); 222 | } 223 | } 224 | 225 | function errorCatcher(chart, callback) { 226 | try { 227 | callback(chart); 228 | } catch (err) { 229 | chartError(chart.element, err.message); 230 | throw err; 231 | } 232 | } 233 | 234 | function fetchDataSource(chart, callback, dataSource) { 235 | if (typeof dataSource === "string") { 236 | pushRequest(chart.element, dataSource, function (data, textStatus, jqXHR) { 237 | chart.rawData = data; 238 | errorCatcher(chart, callback); 239 | }); 240 | } else { 241 | chart.rawData = dataSource; 242 | errorCatcher(chart, callback); 243 | } 244 | } 245 | 246 | function addDownloadButton(chart) { 247 | var element = chart.element; 248 | var link = document.createElement("a"); 249 | link.download = chart.options.download === true ? "chart.png" : chart.options.download; // http://caniuse.com/download 250 | link.style.position = "absolute"; 251 | link.style.top = "20px"; 252 | link.style.right = "20px"; 253 | link.style.zIndex = 1000; 254 | link.style.lineHeight = "20px"; 255 | link.target = "_blank"; // for safari 256 | var image = document.createElement("img"); 257 | image.alt = "Download"; 258 | image.style.border = "none"; 259 | // icon from font-awesome 260 | // http://fa2png.io/ 261 | image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAABCFBMVEUAAADMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMywEsqxAAAAV3RSTlMAAQIDBggJCgsMDQ4PERQaHB0eISIjJCouLzE0OTo/QUJHSUpLTU5PUllhYmltcHh5foWLjI+SlaCio6atr7S1t7m6vsHHyM7R2tze5Obo7fHz9ff5+/1hlxK2AAAA30lEQVQYGUXBhVYCQQBA0TdYWAt2d3d3YWAHyur7/z9xgD16Lw0DW+XKx+1GgX+FRzM3HWQWrHl5N/oapW5RPe0PkBu+UYeICvozTWZVK23Ao04B79oJrOsJDOoxkZoQPWgX29pHpCZEk7rEvQYiNSFq1UMqvlCjJkRBS1R8hb00Vb/TajtBL7nTHE1X1vyMQF732dQhyF2o6SAwrzP06iUQzvwsArlnzcOdrgBhJyHa1QOgO9U1GsKuvjUTjavliZYQ8nNPapG6sap/3nrIdJ6bOWzmX/fy0XVpfzZP3S8OJT3g9EEiJwAAAABJRU5ErkJggg=="; 262 | link.appendChild(image); 263 | element.style.position = "relative"; 264 | 265 | chart.downloadAttached = true; 266 | 267 | // mouseenter 268 | addEvent(element, "mouseover", function(e) { 269 | var related = e.relatedTarget; 270 | // check download option again to ensure it wasn't changed 271 | if (!related || (related !== this && !childOf(this, related)) && chart.options.download) { 272 | link.href = chart.toImage(); 273 | element.appendChild(link); 274 | } 275 | }); 276 | 277 | // mouseleave 278 | addEvent(element, "mouseout", function(e) { 279 | var related = e.relatedTarget; 280 | if (!related || (related !== this && !childOf(this, related))) { 281 | if (link.parentNode) { 282 | link.parentNode.removeChild(link); 283 | } 284 | } 285 | }); 286 | } 287 | 288 | // http://stackoverflow.com/questions/10149963/adding-event-listener-cross-browser 289 | function addEvent(elem, event, fn) { 290 | if (elem.addEventListener) { 291 | elem.addEventListener(event, fn, false); 292 | } else { 293 | elem.attachEvent("on" + event, function() { 294 | // set the this pointer same as addEventListener when fn is called 295 | return(fn.call(elem, window.event)); 296 | }); 297 | } 298 | } 299 | 300 | // https://gist.github.com/shawnbot/4166283 301 | function childOf(p, c) { 302 | if (p === c) return false; 303 | while (c && c !== p) c = c.parentNode; 304 | return c === p; 305 | } 306 | 307 | // type conversions 308 | 309 | function toStr(n) { 310 | return "" + n; 311 | } 312 | 313 | function toFloat(n) { 314 | return parseFloat(n); 315 | } 316 | 317 | function toDate(n) { 318 | var matches, year, month, day; 319 | if (typeof n !== "object") { 320 | if (typeof n === "number") { 321 | n = new Date(n * 1000); // ms 322 | } else if ((matches = n.match(DATE_PATTERN))) { 323 | year = parseInt(matches[1], 10); 324 | month = parseInt(matches[3], 10) - 1; 325 | day = parseInt(matches[5], 10); 326 | return new Date(year, month, day); 327 | } else { // str 328 | // try our best to get the str into iso8601 329 | // TODO be smarter about this 330 | var str = n.replace(/ /, "T").replace(" ", "").replace("UTC", "Z"); 331 | n = parseISO8601(str) || new Date(n); 332 | } 333 | } 334 | return n; 335 | } 336 | 337 | function toArr(n) { 338 | if (!isArray(n)) { 339 | var arr = [], i; 340 | for (i in n) { 341 | if (n.hasOwnProperty(i)) { 342 | arr.push([i, n[i]]); 343 | } 344 | } 345 | n = arr; 346 | } 347 | return n; 348 | } 349 | 350 | function sortByTime(a, b) { 351 | return a[0].getTime() - b[0].getTime(); 352 | } 353 | 354 | function sortByNumberSeries(a, b) { 355 | return a[0] - b[0]; 356 | } 357 | 358 | function sortByNumber(a, b) { 359 | return a - b; 360 | } 361 | 362 | function loadAdapters() { 363 | if (!HighchartsAdapter && "Highcharts" in window) { 364 | HighchartsAdapter = new function () { 365 | var Highcharts = window.Highcharts; 366 | 367 | this.name = "highcharts"; 368 | 369 | var defaultOptions = { 370 | chart: {}, 371 | xAxis: { 372 | title: { 373 | text: null 374 | }, 375 | labels: { 376 | style: { 377 | fontSize: "12px" 378 | } 379 | } 380 | }, 381 | yAxis: { 382 | title: { 383 | text: null 384 | }, 385 | labels: { 386 | style: { 387 | fontSize: "12px" 388 | } 389 | } 390 | }, 391 | title: { 392 | text: null 393 | }, 394 | credits: { 395 | enabled: false 396 | }, 397 | legend: { 398 | borderWidth: 0 399 | }, 400 | tooltip: { 401 | style: { 402 | fontSize: "12px" 403 | } 404 | }, 405 | plotOptions: { 406 | areaspline: {}, 407 | series: { 408 | marker: {} 409 | } 410 | } 411 | }; 412 | 413 | var hideLegend = function (options, legend, hideLegend) { 414 | if (legend !== undefined) { 415 | options.legend.enabled = !!legend; 416 | if (legend && legend !== true) { 417 | if (legend === "top" || legend === "bottom") { 418 | options.legend.verticalAlign = legend; 419 | } else { 420 | options.legend.layout = "vertical"; 421 | options.legend.verticalAlign = "middle"; 422 | options.legend.align = legend; 423 | } 424 | } 425 | } else if (hideLegend) { 426 | options.legend.enabled = false; 427 | } 428 | }; 429 | 430 | var setTitle = function (options, title) { 431 | options.title.text = title; 432 | }; 433 | 434 | var setMin = function (options, min) { 435 | options.yAxis.min = min; 436 | }; 437 | 438 | var setMax = function (options, max) { 439 | options.yAxis.max = max; 440 | }; 441 | 442 | var setStacked = function (options, stacked) { 443 | options.plotOptions.series.stacking = stacked ? "normal" : null; 444 | }; 445 | 446 | var setXtitle = function (options, title) { 447 | options.xAxis.title.text = title; 448 | }; 449 | 450 | var setYtitle = function (options, title) { 451 | options.yAxis.title.text = title; 452 | }; 453 | 454 | var jsOptions = jsOptionsFunc(defaultOptions, hideLegend, setTitle, setMin, setMax, setStacked, setXtitle, setYtitle); 455 | 456 | this.renderLineChart = function (chart, chartType) { 457 | chartType = chartType || "spline"; 458 | var chartOptions = {}; 459 | if (chartType === "areaspline") { 460 | chartOptions = { 461 | plotOptions: { 462 | areaspline: { 463 | stacking: "normal" 464 | }, 465 | area: { 466 | stacking: "normal" 467 | }, 468 | series: { 469 | marker: { 470 | enabled: false 471 | } 472 | } 473 | } 474 | }; 475 | } 476 | 477 | if (chart.options.curve === false) { 478 | if (chartType === "areaspline") { 479 | chartType = "area"; 480 | } else if (chartType === "spline") { 481 | chartType = "line"; 482 | } 483 | } 484 | 485 | var options = jsOptions(chart, chart.options, chartOptions), data, i, j; 486 | options.xAxis.type = chart.discrete ? "category" : "datetime"; 487 | if (!options.chart.type) { 488 | options.chart.type = chartType; 489 | } 490 | options.chart.renderTo = chart.element.id; 491 | 492 | var series = chart.data; 493 | for (i = 0; i < series.length; i++) { 494 | data = series[i].data; 495 | if (!chart.discrete) { 496 | for (j = 0; j < data.length; j++) { 497 | data[j][0] = data[j][0].getTime(); 498 | } 499 | } 500 | series[i].marker = {symbol: "circle"}; 501 | if (chart.options.points === false) { 502 | series[i].marker.enabled = false; 503 | } 504 | } 505 | options.series = series; 506 | chart.chart = new Highcharts.Chart(options); 507 | }; 508 | 509 | this.renderScatterChart = function (chart) { 510 | var chartOptions = {}; 511 | var options = jsOptions(chart, chart.options, chartOptions); 512 | options.chart.type = "scatter"; 513 | options.chart.renderTo = chart.element.id; 514 | options.series = chart.data; 515 | chart.chart = new Highcharts.Chart(options); 516 | }; 517 | 518 | this.renderPieChart = function (chart) { 519 | var chartOptions = merge(defaultOptions, {}); 520 | 521 | if (chart.options.colors) { 522 | chartOptions.colors = chart.options.colors; 523 | } 524 | if (chart.options.donut) { 525 | chartOptions.plotOptions = {pie: {innerSize: "50%"}}; 526 | } 527 | 528 | if ("legend" in chart.options) { 529 | hideLegend(chartOptions, chart.options.legend); 530 | } 531 | 532 | if (chart.options.title) { 533 | setTitle(chartOptions, chart.options.title); 534 | } 535 | 536 | var options = merge(chartOptions, chart.options.library || {}); 537 | options.chart.renderTo = chart.element.id; 538 | options.series = [{ 539 | type: "pie", 540 | name: chart.options.label || "Value", 541 | data: chart.data 542 | }]; 543 | chart.chart = new Highcharts.Chart(options); 544 | }; 545 | 546 | this.renderColumnChart = function (chart, chartType) { 547 | chartType = chartType || "column"; 548 | var series = chart.data; 549 | var options = jsOptions(chart, chart.options), i, j, s, d, rows = [], categories = []; 550 | options.chart.type = chartType; 551 | options.chart.renderTo = chart.element.id; 552 | 553 | for (i = 0; i < series.length; i++) { 554 | s = series[i]; 555 | 556 | for (j = 0; j < s.data.length; j++) { 557 | d = s.data[j]; 558 | if (!rows[d[0]]) { 559 | rows[d[0]] = new Array(series.length); 560 | categories.push(d[0]); 561 | } 562 | rows[d[0]][i] = d[1]; 563 | } 564 | } 565 | 566 | if (chart.options.xtype === "number") { 567 | categories.sort(sortByNumber); 568 | } 569 | 570 | options.xAxis.categories = categories; 571 | 572 | var newSeries = [], d2; 573 | for (i = 0; i < series.length; i++) { 574 | d = []; 575 | for (j = 0; j < categories.length; j++) { 576 | d.push(rows[categories[j]][i] || 0); 577 | } 578 | 579 | d2 = { 580 | name: series[i].name, 581 | data: d 582 | } 583 | if (series[i].stack) { 584 | d2.stack = series[i].stack; 585 | } 586 | 587 | newSeries.push(d2); 588 | } 589 | options.series = newSeries; 590 | 591 | chart.chart = new Highcharts.Chart(options); 592 | }; 593 | 594 | var self = this; 595 | 596 | this.renderBarChart = function (chart) { 597 | self.renderColumnChart(chart, "bar"); 598 | }; 599 | 600 | this.renderAreaChart = function (chart) { 601 | self.renderLineChart(chart, "areaspline"); 602 | }; 603 | }; 604 | adapters.push(HighchartsAdapter); 605 | } 606 | if (!GoogleChartsAdapter && window.google && (window.google.setOnLoadCallback || window.google.charts)) { 607 | GoogleChartsAdapter = new function () { 608 | var google = window.google; 609 | 610 | this.name = "google"; 611 | 612 | var loaded = {}; 613 | var callbacks = []; 614 | 615 | var runCallbacks = function () { 616 | var cb, call; 617 | for (var i = 0; i < callbacks.length; i++) { 618 | cb = callbacks[i]; 619 | call = google.visualization && ((cb.pack === "corechart" && google.visualization.LineChart) || (cb.pack === "timeline" && google.visualization.Timeline)); 620 | if (call) { 621 | cb.callback(); 622 | callbacks.splice(i, 1); 623 | i--; 624 | } 625 | } 626 | }; 627 | 628 | var waitForLoaded = function (pack, callback) { 629 | if (!callback) { 630 | callback = pack; 631 | pack = "corechart"; 632 | } 633 | 634 | callbacks.push({pack: pack, callback: callback}); 635 | 636 | if (loaded[pack]) { 637 | runCallbacks(); 638 | } else { 639 | loaded[pack] = true; 640 | 641 | // https://groups.google.com/forum/#!topic/google-visualization-api/fMKJcyA2yyI 642 | var loadOptions = { 643 | packages: [pack], 644 | callback: runCallbacks 645 | }; 646 | if (config.language) { 647 | loadOptions.language = config.language; 648 | } 649 | if (pack === "corechart" && config.mapsApiKey) { 650 | loadOptions.mapsApiKey = config.mapsApiKey; 651 | } 652 | 653 | if (window.google.setOnLoadCallback) { 654 | google.load("visualization", "1", loadOptions); 655 | } else { 656 | google.charts.load("current", loadOptions); 657 | } 658 | } 659 | }; 660 | 661 | // Set chart options 662 | var defaultOptions = { 663 | chartArea: {}, 664 | fontName: "'Lucida Grande', 'Lucida Sans Unicode', Verdana, Arial, Helvetica, sans-serif", 665 | pointSize: 6, 666 | legend: { 667 | textStyle: { 668 | fontSize: 12, 669 | color: "#444" 670 | }, 671 | alignment: "center", 672 | position: "right" 673 | }, 674 | curveType: "function", 675 | hAxis: { 676 | textStyle: { 677 | color: "#666", 678 | fontSize: 12 679 | }, 680 | titleTextStyle: {}, 681 | gridlines: { 682 | color: "transparent" 683 | }, 684 | baselineColor: "#ccc", 685 | viewWindow: {} 686 | }, 687 | vAxis: { 688 | textStyle: { 689 | color: "#666", 690 | fontSize: 12 691 | }, 692 | titleTextStyle: {}, 693 | baselineColor: "#ccc", 694 | viewWindow: {} 695 | }, 696 | tooltip: { 697 | textStyle: { 698 | color: "#666", 699 | fontSize: 12 700 | } 701 | } 702 | }; 703 | 704 | var hideLegend = function (options, legend, hideLegend) { 705 | if (legend !== undefined) { 706 | var position; 707 | if (!legend) { 708 | position = "none"; 709 | } else if (legend === true) { 710 | position = "right"; 711 | } else { 712 | position = legend; 713 | } 714 | options.legend.position = position; 715 | } else if (hideLegend) { 716 | options.legend.position = "none"; 717 | } 718 | }; 719 | 720 | var setTitle = function (options, title) { 721 | options.title = title; 722 | options.titleTextStyle = {color: "#333", fontSize: "20px"}; 723 | }; 724 | 725 | var setMin = function (options, min) { 726 | options.vAxis.viewWindow.min = min; 727 | }; 728 | 729 | var setMax = function (options, max) { 730 | options.vAxis.viewWindow.max = max; 731 | }; 732 | 733 | var setBarMin = function (options, min) { 734 | options.hAxis.viewWindow.min = min; 735 | }; 736 | 737 | var setBarMax = function (options, max) { 738 | options.hAxis.viewWindow.max = max; 739 | }; 740 | 741 | var setStacked = function (options, stacked) { 742 | options.isStacked = !!stacked; 743 | }; 744 | 745 | var setXtitle = function (options, title) { 746 | options.hAxis.title = title; 747 | options.hAxis.titleTextStyle.italic = false; 748 | }; 749 | 750 | var setYtitle = function (options, title) { 751 | options.vAxis.title = title; 752 | options.vAxis.titleTextStyle.italic = false; 753 | }; 754 | 755 | var jsOptions = jsOptionsFunc(defaultOptions, hideLegend, setTitle, setMin, setMax, setStacked, setXtitle, setYtitle); 756 | 757 | // cant use object as key 758 | var createDataTable = function (series, columnType, xtype) { 759 | var i, j, s, d, key, rows = [], sortedLabels = []; 760 | for (i = 0; i < series.length; i++) { 761 | s = series[i]; 762 | 763 | for (j = 0; j < s.data.length; j++) { 764 | d = s.data[j]; 765 | key = (columnType === "datetime") ? d[0].getTime() : d[0]; 766 | if (!rows[key]) { 767 | rows[key] = new Array(series.length); 768 | sortedLabels.push(key); 769 | } 770 | rows[key][i] = toFloat(d[1]); 771 | } 772 | } 773 | 774 | var rows2 = []; 775 | var day = true; 776 | var value; 777 | for (var j = 0; j < sortedLabels.length; j++) { 778 | var i = sortedLabels[j]; 779 | if (columnType === "datetime") { 780 | value = new Date(toFloat(i)); 781 | day = day && isDay(value); 782 | } else if (columnType === "number") { 783 | value = toFloat(i); 784 | } else { 785 | value = i; 786 | } 787 | rows2.push([value].concat(rows[i])); 788 | } 789 | if (columnType === "datetime") { 790 | rows2.sort(sortByTime); 791 | } else if (columnType === "number") { 792 | rows2.sort(sortByNumberSeries); 793 | } 794 | 795 | if (xtype === "number") { 796 | rows2.sort(sortByNumberSeries); 797 | 798 | for (var i = 0; i < rows2.length; i++) { 799 | rows2[i][0] = toStr(rows2[i][0]); 800 | } 801 | } 802 | 803 | // create datatable 804 | var data = new google.visualization.DataTable(); 805 | columnType = columnType === "datetime" && day ? "date" : columnType; 806 | data.addColumn(columnType, ""); 807 | for (i = 0; i < series.length; i++) { 808 | data.addColumn("number", series[i].name); 809 | } 810 | data.addRows(rows2); 811 | 812 | return data; 813 | }; 814 | 815 | var resize = function (callback) { 816 | if (window.attachEvent) { 817 | window.attachEvent("onresize", callback); 818 | } else if (window.addEventListener) { 819 | window.addEventListener("resize", callback, true); 820 | } 821 | callback(); 822 | }; 823 | 824 | this.renderLineChart = function (chart) { 825 | waitForLoaded(function () { 826 | var chartOptions = {}; 827 | 828 | if (chart.options.curve === false) { 829 | chartOptions.curveType = "none"; 830 | } 831 | 832 | if (chart.options.points === false) { 833 | chartOptions.pointSize = 0; 834 | } 835 | 836 | var options = jsOptions(chart, chart.options, chartOptions); 837 | var columnType = chart.discrete ? "string" : "datetime"; 838 | if (chart.options.xtype === "number") { 839 | columnType = "number"; 840 | } 841 | var data = createDataTable(chart.data, columnType); 842 | chart.chart = new google.visualization.LineChart(chart.element); 843 | resize(function () { 844 | chart.chart.draw(data, options); 845 | }); 846 | }); 847 | }; 848 | 849 | this.renderPieChart = function (chart) { 850 | waitForLoaded(function () { 851 | var chartOptions = { 852 | chartArea: { 853 | top: "10%", 854 | height: "80%" 855 | }, 856 | legend: {} 857 | }; 858 | if (chart.options.colors) { 859 | chartOptions.colors = chart.options.colors; 860 | } 861 | if (chart.options.donut) { 862 | chartOptions.pieHole = 0.5; 863 | } 864 | if ("legend" in chart.options) { 865 | hideLegend(chartOptions, chart.options.legend); 866 | } 867 | if (chart.options.title) { 868 | setTitle(chartOptions, chart.options.title); 869 | } 870 | var options = merge(merge(defaultOptions, chartOptions), chart.options.library || {}); 871 | 872 | var data = new google.visualization.DataTable(); 873 | data.addColumn("string", ""); 874 | data.addColumn("number", "Value"); 875 | data.addRows(chart.data); 876 | 877 | chart.chart = new google.visualization.PieChart(chart.element); 878 | resize(function () { 879 | chart.chart.draw(data, options); 880 | }); 881 | }); 882 | }; 883 | 884 | this.renderColumnChart = function (chart) { 885 | waitForLoaded(function () { 886 | var options = jsOptions(chart, chart.options); 887 | var data = createDataTable(chart.data, "string", chart.options.xtype); 888 | chart.chart = new google.visualization.ColumnChart(chart.element); 889 | resize(function () { 890 | chart.chart.draw(data, options); 891 | }); 892 | }); 893 | }; 894 | 895 | this.renderBarChart = function (chart) { 896 | waitForLoaded(function () { 897 | var chartOptions = { 898 | hAxis: { 899 | gridlines: { 900 | color: "#ccc" 901 | } 902 | } 903 | }; 904 | var options = jsOptionsFunc(defaultOptions, hideLegend, setTitle, setBarMin, setBarMax, setStacked, setXtitle, setYtitle)(chart, chart.options, chartOptions); 905 | var data = createDataTable(chart.data, "string", chart.options.xtype); 906 | chart.chart = new google.visualization.BarChart(chart.element); 907 | resize(function () { 908 | chart.chart.draw(data, options); 909 | }); 910 | }); 911 | }; 912 | 913 | this.renderAreaChart = function (chart) { 914 | waitForLoaded(function () { 915 | var chartOptions = { 916 | isStacked: true, 917 | pointSize: 0, 918 | areaOpacity: 0.5 919 | }; 920 | 921 | var options = jsOptions(chart, chart.options, chartOptions); 922 | var columnType = chart.discrete ? "string" : "datetime"; 923 | if (chart.options.xtype === "number") { 924 | columnType = "number"; 925 | } 926 | var data = createDataTable(chart.data, columnType); 927 | chart.chart = new google.visualization.AreaChart(chart.element); 928 | resize(function () { 929 | chart.chart.draw(data, options); 930 | }); 931 | }); 932 | }; 933 | 934 | this.renderGeoChart = function (chart) { 935 | waitForLoaded(function () { 936 | var chartOptions = { 937 | legend: "none", 938 | colorAxis: { 939 | colors: chart.options.colors || ["#f6c7b6", "#ce502d"] 940 | } 941 | }; 942 | var options = merge(merge(defaultOptions, chartOptions), chart.options.library || {}); 943 | 944 | var data = new google.visualization.DataTable(); 945 | data.addColumn("string", ""); 946 | data.addColumn("number", chart.options.label || "Value"); 947 | data.addRows(chart.data); 948 | 949 | chart.chart = new google.visualization.GeoChart(chart.element); 950 | resize(function () { 951 | chart.chart.draw(data, options); 952 | }); 953 | }); 954 | }; 955 | 956 | this.renderScatterChart = function (chart) { 957 | waitForLoaded(function () { 958 | var chartOptions = {}; 959 | var options = jsOptions(chart, chart.options, chartOptions); 960 | 961 | var series = chart.data, rows2 = [], i, j, data, d; 962 | for (i = 0; i < series.length; i++) { 963 | d = series[i].data; 964 | for (j = 0; j < d.length; j++) { 965 | var row = new Array(series.length + 1); 966 | row[0] = d[j][0]; 967 | row[i + 1] = d[j][1]; 968 | rows2.push(row); 969 | } 970 | } 971 | 972 | var data = new google.visualization.DataTable(); 973 | data.addColumn("number", ""); 974 | for (i = 0; i < series.length; i++) { 975 | data.addColumn("number", series[i].name); 976 | } 977 | data.addRows(rows2); 978 | 979 | chart.chart = new google.visualization.ScatterChart(chart.element); 980 | resize(function () { 981 | chart.chart.draw(data, options); 982 | }); 983 | }); 984 | }; 985 | 986 | this.renderTimeline = function (chart) { 987 | waitForLoaded("timeline", function () { 988 | var chartOptions = { 989 | legend: "none" 990 | }; 991 | 992 | if (chart.options.colors) { 993 | chartOptions.colors = chart.options.colors; 994 | } 995 | var options = merge(merge(defaultOptions, chartOptions), chart.options.library || {}); 996 | 997 | var data = new google.visualization.DataTable(); 998 | data.addColumn({type: "string", id: "Name"}); 999 | data.addColumn({type: "date", id: "Start"}); 1000 | data.addColumn({type: "date", id: "End"}); 1001 | data.addRows(chart.data); 1002 | 1003 | chart.element.style.lineHeight = "normal"; 1004 | chart.chart = new google.visualization.Timeline(chart.element); 1005 | 1006 | resize(function () { 1007 | chart.chart.draw(data, options); 1008 | }); 1009 | }); 1010 | }; 1011 | }; 1012 | 1013 | adapters.push(GoogleChartsAdapter); 1014 | } 1015 | if (!ChartjsAdapter && "Chart" in window) { 1016 | ChartjsAdapter = new function () { 1017 | var Chart = window.Chart; 1018 | 1019 | this.name = "chartjs"; 1020 | 1021 | var baseOptions = { 1022 | maintainAspectRatio: false, 1023 | animation: false, 1024 | tooltips: { 1025 | displayColors: false 1026 | }, 1027 | legend: {}, 1028 | title: {fontSize: 20, fontColor: "#333"} 1029 | }; 1030 | 1031 | var defaultOptions = { 1032 | scales: { 1033 | yAxes: [ 1034 | { 1035 | ticks: { 1036 | maxTicksLimit: 4 1037 | }, 1038 | scaleLabel: { 1039 | fontSize: 16, 1040 | // fontStyle: "bold", 1041 | fontColor: "#333" 1042 | } 1043 | } 1044 | ], 1045 | xAxes: [ 1046 | { 1047 | gridLines: { 1048 | drawOnChartArea: false 1049 | }, 1050 | scaleLabel: { 1051 | fontSize: 16, 1052 | // fontStyle: "bold", 1053 | fontColor: "#333" 1054 | }, 1055 | time: {}, 1056 | ticks: {} 1057 | } 1058 | ] 1059 | } 1060 | }; 1061 | 1062 | // http://there4.io/2012/05/02/google-chart-color-list/ 1063 | var defaultColors = [ 1064 | "#3366CC", "#DC3912", "#FF9900", "#109618", "#990099", "#3B3EAC", "#0099C6", 1065 | "#DD4477", "#66AA00", "#B82E2E", "#316395", "#994499", "#22AA99", "#AAAA11", 1066 | "#6633CC", "#E67300", "#8B0707", "#329262", "#5574A6", "#651067" 1067 | ]; 1068 | 1069 | var hideLegend = function (options, legend, hideLegend) { 1070 | if (legend !== undefined) { 1071 | options.legend.display = !!legend; 1072 | if (legend && legend !== true) { 1073 | options.legend.position = legend; 1074 | } 1075 | } else if (hideLegend) { 1076 | options.legend.display = false; 1077 | } 1078 | }; 1079 | 1080 | var setTitle = function (options, title) { 1081 | options.title.display = true; 1082 | options.title.text = title; 1083 | }; 1084 | 1085 | var setMin = function (options, min) { 1086 | if (min !== null) { 1087 | options.scales.yAxes[0].ticks.min = toFloat(min); 1088 | } 1089 | }; 1090 | 1091 | var setMax = function (options, max) { 1092 | options.scales.yAxes[0].ticks.max = toFloat(max); 1093 | }; 1094 | 1095 | var setBarMin = function (options, min) { 1096 | if (min !== null) { 1097 | options.scales.xAxes[0].ticks.min = toFloat(min); 1098 | } 1099 | }; 1100 | 1101 | var setBarMax = function (options, max) { 1102 | options.scales.xAxes[0].ticks.max = toFloat(max); 1103 | }; 1104 | 1105 | var setStacked = function (options, stacked) { 1106 | options.scales.xAxes[0].stacked = !!stacked; 1107 | options.scales.yAxes[0].stacked = !!stacked; 1108 | }; 1109 | 1110 | var setXtitle = function (options, title) { 1111 | options.scales.xAxes[0].scaleLabel.display = true; 1112 | options.scales.xAxes[0].scaleLabel.labelString = title; 1113 | }; 1114 | 1115 | var setYtitle = function (options, title) { 1116 | options.scales.yAxes[0].scaleLabel.display = true; 1117 | options.scales.yAxes[0].scaleLabel.labelString = title; 1118 | }; 1119 | 1120 | var drawChart = function(chart, type, data, options) { 1121 | if (chart.chart) { 1122 | chart.chart.destroy(); 1123 | } else { 1124 | chart.element.innerHTML = ""; 1125 | } 1126 | 1127 | var ctx = chart.element.getElementsByTagName("CANVAS")[0]; 1128 | chart.chart = new Chart(ctx, { 1129 | type: type, 1130 | data: data, 1131 | options: options 1132 | }); 1133 | }; 1134 | 1135 | // http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb 1136 | var addOpacity = function(hex, opacity) { 1137 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 1138 | return result ? "rgba(" + parseInt(result[1], 16) + ", " + parseInt(result[2], 16) + ", " + parseInt(result[3], 16) + ", " + opacity + ")" : hex; 1139 | }; 1140 | 1141 | var setLabelSize = function (chart, data, options) { 1142 | var maxLabelSize = Math.ceil(chart.element.offsetWidth / 4.0 / data.labels.length); 1143 | if (maxLabelSize > 25) { 1144 | maxLabelSize = 25; 1145 | } 1146 | options.scales.xAxes[0].ticks.callback = function (value) { 1147 | value = toStr(value); 1148 | if (value.length > maxLabelSize) { 1149 | return value.substring(0, maxLabelSize - 2) + "..."; 1150 | } else { 1151 | return value; 1152 | } 1153 | }; 1154 | }; 1155 | 1156 | var jsOptions = jsOptionsFunc(merge(baseOptions, defaultOptions), hideLegend, setTitle, setMin, setMax, setStacked, setXtitle, setYtitle); 1157 | 1158 | var createDataTable = function (chart, options, chartType) { 1159 | var datasets = []; 1160 | var labels = []; 1161 | 1162 | var colors = chart.options.colors || defaultColors; 1163 | 1164 | var day = true; 1165 | var week = true; 1166 | var dayOfWeek; 1167 | var month = true; 1168 | var year = true; 1169 | var hour = true; 1170 | var minute = true; 1171 | var detectType = (chartType === "line" || chartType === "area") && !chart.discrete; 1172 | 1173 | var series = chart.data; 1174 | 1175 | var sortedLabels = []; 1176 | 1177 | var i, j, s, d, key, rows = []; 1178 | for (i = 0; i < series.length; i++) { 1179 | s = series[i]; 1180 | 1181 | for (j = 0; j < s.data.length; j++) { 1182 | d = s.data[j]; 1183 | key = detectType ? d[0].getTime() : d[0]; 1184 | if (!rows[key]) { 1185 | rows[key] = new Array(series.length); 1186 | } 1187 | rows[key][i] = toFloat(d[1]); 1188 | if (sortedLabels.indexOf(key) === -1) { 1189 | sortedLabels.push(key); 1190 | } 1191 | } 1192 | } 1193 | 1194 | if (detectType || chart.options.xtype === "number") { 1195 | sortedLabels.sort(sortByNumber); 1196 | } 1197 | 1198 | var rows2 = []; 1199 | for (j = 0; j < series.length; j++) { 1200 | rows2.push([]); 1201 | } 1202 | 1203 | var value; 1204 | var k; 1205 | for (k = 0; k < sortedLabels.length; k++) { 1206 | i = sortedLabels[k]; 1207 | if (detectType) { 1208 | value = new Date(toFloat(i)); 1209 | // TODO make this efficient 1210 | day = day && isDay(value); 1211 | if (!dayOfWeek) { 1212 | dayOfWeek = value.getDay(); 1213 | } 1214 | week = week && isWeek(value, dayOfWeek); 1215 | month = month && isMonth(value); 1216 | year = year && isYear(value); 1217 | hour = hour && isHour(value); 1218 | minute = minute && isMinute(value); 1219 | } else { 1220 | value = i; 1221 | } 1222 | labels.push(value); 1223 | for (j = 0; j < series.length; j++) { 1224 | // Chart.js doesn't like undefined 1225 | rows2[j].push(rows[i][j] === undefined ? null : rows[i][j]); 1226 | } 1227 | } 1228 | 1229 | for (i = 0; i < series.length; i++) { 1230 | s = series[i]; 1231 | 1232 | var color = s.color || colors[i]; 1233 | var backgroundColor = chartType !== "line" ? addOpacity(color, 0.5) : color; 1234 | 1235 | var dataset = { 1236 | label: s.name, 1237 | data: rows2[i], 1238 | fill: chartType === "area", 1239 | borderColor: color, 1240 | backgroundColor: backgroundColor, 1241 | pointBackgroundColor: color, 1242 | borderWidth: 2 1243 | }; 1244 | 1245 | if (s.stack) { 1246 | dataset.stack = s.stack; 1247 | } 1248 | 1249 | if (chart.options.curve === false) { 1250 | dataset.lineTension = 0; 1251 | } 1252 | 1253 | if (chart.options.points === false) { 1254 | dataset.pointRadius = 0; 1255 | dataset.pointHitRadius = 5; 1256 | } 1257 | 1258 | datasets.push(merge(dataset, s.library || {})); 1259 | } 1260 | 1261 | if (detectType && labels.length > 0) { 1262 | var minTime = labels[0].getTime(); 1263 | var maxTime = labels[0].getTime(); 1264 | for (i = 1; i < labels.length; i++) { 1265 | value = labels[i].getTime(); 1266 | if (value < minTime) { 1267 | minTime = value; 1268 | } 1269 | if (value > maxTime) { 1270 | maxTime = value; 1271 | } 1272 | } 1273 | 1274 | var timeDiff = (maxTime - minTime) / (86400 * 1000.0); 1275 | 1276 | if (!options.scales.xAxes[0].time.unit) { 1277 | var step; 1278 | if (year || timeDiff > 365 * 10) { 1279 | options.scales.xAxes[0].time.unit = "year"; 1280 | step = 365; 1281 | } else if (month || timeDiff > 30 * 10) { 1282 | options.scales.xAxes[0].time.unit = "month"; 1283 | step = 30; 1284 | } else if (day || timeDiff > 10) { 1285 | options.scales.xAxes[0].time.unit = "day"; 1286 | step = 1; 1287 | } else if (hour || timeDiff > 0.5) { 1288 | options.scales.xAxes[0].time.displayFormats = {hour: "MMM D, h a"}; 1289 | options.scales.xAxes[0].time.unit = "hour"; 1290 | step = 1 / 24.0; 1291 | } else if (minute) { 1292 | options.scales.xAxes[0].time.displayFormats = {minute: "h:mm a"}; 1293 | options.scales.xAxes[0].time.unit = "minute"; 1294 | step = 1 / 24.0 / 60.0; 1295 | } 1296 | 1297 | if (step && timeDiff > 0) { 1298 | var unitStepSize = Math.ceil(timeDiff / step / (chart.element.offsetWidth / 100.0)); 1299 | if (week && step === 1) { 1300 | unitStepSize = Math.ceil(unitStepSize / 7.0) * 7; 1301 | } 1302 | options.scales.xAxes[0].time.unitStepSize = unitStepSize; 1303 | } 1304 | } 1305 | 1306 | if (!options.scales.xAxes[0].time.tooltipFormat) { 1307 | if (day) { 1308 | options.scales.xAxes[0].time.tooltipFormat = "ll"; 1309 | } else if (hour) { 1310 | options.scales.xAxes[0].time.tooltipFormat = "MMM D, h a"; 1311 | } else if (minute) { 1312 | options.scales.xAxes[0].time.tooltipFormat = "h:mm a"; 1313 | } 1314 | } 1315 | } 1316 | 1317 | var data = { 1318 | labels: labels, 1319 | datasets: datasets 1320 | }; 1321 | 1322 | return data; 1323 | }; 1324 | 1325 | this.renderLineChart = function (chart, chartType) { 1326 | if (chart.options.xtype === "number") { 1327 | return self.renderScatterChart(chart, chartType, true); 1328 | } 1329 | 1330 | var chartOptions = {}; 1331 | if (chartType === "area") { 1332 | // TODO fix area stacked 1333 | // chartOptions.stacked = true; 1334 | } 1335 | // fix for https://github.com/chartjs/Chart.js/issues/2441 1336 | if (!chart.options.max && allZeros(chart.data)) { 1337 | chartOptions.max = 1; 1338 | } 1339 | 1340 | var options = jsOptions(chart, merge(chartOptions, chart.options)); 1341 | 1342 | var data = createDataTable(chart, options, chartType || "line"); 1343 | 1344 | options.scales.xAxes[0].type = chart.discrete ? "category" : "time"; 1345 | 1346 | drawChart(chart, "line", data, options); 1347 | }; 1348 | 1349 | this.renderPieChart = function (chart) { 1350 | var options = merge({}, baseOptions); 1351 | if (chart.options.donut) { 1352 | options.cutoutPercentage = 50; 1353 | } 1354 | 1355 | if ("legend" in chart.options) { 1356 | hideLegend(options, chart.options.legend); 1357 | } 1358 | 1359 | if (chart.options.title) { 1360 | setTitle(options, chart.options.title); 1361 | } 1362 | 1363 | options = merge(options, chart.options.library || {}); 1364 | 1365 | var labels = []; 1366 | var values = []; 1367 | for (var i = 0; i < chart.data.length; i++) { 1368 | var point = chart.data[i]; 1369 | labels.push(point[0]); 1370 | values.push(point[1]); 1371 | } 1372 | 1373 | var data = { 1374 | labels: labels, 1375 | datasets: [ 1376 | { 1377 | data: values, 1378 | backgroundColor: chart.options.colors || defaultColors 1379 | } 1380 | ] 1381 | }; 1382 | 1383 | drawChart(chart, "pie", data, options); 1384 | }; 1385 | 1386 | this.renderColumnChart = function (chart, chartType) { 1387 | var options; 1388 | if (chartType === "bar") { 1389 | options = jsOptionsFunc(merge(baseOptions, defaultOptions), hideLegend, setTitle, setBarMin, setBarMax, setStacked, setXtitle, setYtitle)(chart, chart.options); 1390 | } else { 1391 | options = jsOptions(chart, chart.options); 1392 | } 1393 | var data = createDataTable(chart, options, "column"); 1394 | setLabelSize(chart, data, options); 1395 | drawChart(chart, (chartType === "bar" ? "horizontalBar" : "bar"), data, options); 1396 | }; 1397 | 1398 | var self = this; 1399 | 1400 | this.renderAreaChart = function (chart) { 1401 | self.renderLineChart(chart, "area"); 1402 | }; 1403 | 1404 | this.renderBarChart = function (chart) { 1405 | self.renderColumnChart(chart, "bar"); 1406 | }; 1407 | 1408 | this.renderScatterChart = function (chart, chartType, lineChart) { 1409 | chartType = chartType || "line"; 1410 | 1411 | var options = jsOptions(chart, chart.options); 1412 | 1413 | var colors = chart.options.colors || defaultColors; 1414 | 1415 | var datasets = []; 1416 | var series = chart.data; 1417 | for (var i = 0; i < series.length; i++) { 1418 | var s = series[i]; 1419 | var d = []; 1420 | for (var j = 0; j < s.data.length; j++) { 1421 | var point = { 1422 | x: toFloat(s.data[j][0]), 1423 | y: toFloat(s.data[j][1]) 1424 | }; 1425 | if (chartType === "bubble") { 1426 | point.r = toFloat(s.data[j][2]); 1427 | } 1428 | d.push(point); 1429 | } 1430 | 1431 | var color = s.color || colors[i]; 1432 | var backgroundColor = chartType === "area" ? addOpacity(color, 0.5) : color; 1433 | 1434 | datasets.push({ 1435 | label: s.name, 1436 | showLine: lineChart || false, 1437 | data: d, 1438 | borderColor: color, 1439 | backgroundColor: backgroundColor, 1440 | pointBackgroundColor: color, 1441 | fill: chartType === "area" 1442 | }) 1443 | } 1444 | 1445 | if (chartType === "area") { 1446 | chartType = "line"; 1447 | } 1448 | 1449 | var data = {datasets: datasets}; 1450 | 1451 | options.scales.xAxes[0].type = "linear"; 1452 | options.scales.xAxes[0].position = "bottom"; 1453 | 1454 | drawChart(chart, chartType, data, options); 1455 | }; 1456 | 1457 | this.renderBubbleChart = function (chart) { 1458 | this.renderScatterChart(chart, "bubble"); 1459 | }; 1460 | }; 1461 | 1462 | adapters.unshift(ChartjsAdapter); 1463 | } 1464 | } 1465 | 1466 | function renderChart(chartType, chart) { 1467 | callAdapter(chartType, chart); 1468 | if (chart.options.download && !chart.downloadAttached && chart.adapter === "chartjs") { 1469 | addDownloadButton(chart); 1470 | } 1471 | } 1472 | 1473 | // TODO remove chartType if cross-browser way 1474 | // to get the name of the chart class 1475 | function callAdapter(chartType, chart) { 1476 | var i, adapter, fnName, adapterName; 1477 | fnName = "render" + chartType; 1478 | adapterName = chart.options.adapter; 1479 | 1480 | loadAdapters(); 1481 | 1482 | for (i = 0; i < adapters.length; i++) { 1483 | adapter = adapters[i]; 1484 | if ((!adapterName || adapterName === adapter.name) && isFunction(adapter[fnName])) { 1485 | chart.adapter = adapter.name; 1486 | return adapter[fnName](chart); 1487 | } 1488 | } 1489 | throw new Error("No adapter found"); 1490 | } 1491 | 1492 | // process data 1493 | 1494 | var toFormattedKey = function (key, keyType) { 1495 | if (keyType === "number") { 1496 | key = toFloat(key); 1497 | } else if (keyType === "datetime") { 1498 | key = toDate(key); 1499 | } else { 1500 | key = toStr(key); 1501 | } 1502 | return key; 1503 | }; 1504 | 1505 | var formatSeriesData = function (data, keyType) { 1506 | var r = [], key, j; 1507 | for (j = 0; j < data.length; j++) { 1508 | if (keyType === "bubble") { 1509 | r.push([toFloat(data[j][0]), toFloat(data[j][1]), toFloat(data[j][2])]); 1510 | } else { 1511 | key = toFormattedKey(data[j][0], keyType); 1512 | r.push([key, toFloat(data[j][1])]); 1513 | } 1514 | } 1515 | if (keyType === "datetime") { 1516 | r.sort(sortByTime); 1517 | } else if (keyType === "number") { 1518 | r.sort(sortByNumberSeries); 1519 | } 1520 | return r; 1521 | }; 1522 | 1523 | function isMinute(d) { 1524 | return d.getMilliseconds() === 0 && d.getSeconds() === 0; 1525 | } 1526 | 1527 | function isHour(d) { 1528 | return isMinute(d) && d.getMinutes() === 0; 1529 | } 1530 | 1531 | function isDay(d) { 1532 | return isHour(d) && d.getHours() === 0; 1533 | } 1534 | 1535 | function isWeek(d, dayOfWeek) { 1536 | return isDay(d) && d.getDay() === dayOfWeek; 1537 | } 1538 | 1539 | function isMonth(d) { 1540 | return isDay(d) && d.getDate() === 1; 1541 | } 1542 | 1543 | function isYear(d) { 1544 | return isMonth(d) && d.getMonth() === 0; 1545 | } 1546 | 1547 | function isDate(obj) { 1548 | return !isNaN(toDate(obj)) && toStr(obj).length >= 6; 1549 | } 1550 | 1551 | function allZeros(data) { 1552 | var i, j, d; 1553 | for (i = 0; i < data.length; i++) { 1554 | d = data[i].data; 1555 | for (j = 0; j < d.length; j++) { 1556 | if (d[j][1] != 0) { 1557 | return false; 1558 | } 1559 | } 1560 | } 1561 | return true; 1562 | } 1563 | 1564 | function detectDiscrete(series) { 1565 | var i, j, data; 1566 | for (i = 0; i < series.length; i++) { 1567 | data = toArr(series[i].data); 1568 | for (j = 0; j < data.length; j++) { 1569 | if (!isDate(data[j][0])) { 1570 | return true; 1571 | } 1572 | } 1573 | } 1574 | return false; 1575 | } 1576 | 1577 | // creates a shallow copy of each element of the array 1578 | // elements are expected to be objects 1579 | function copySeries(series) { 1580 | var newSeries = [], i, j; 1581 | for (i = 0; i < series.length; i++) { 1582 | var copy = {} 1583 | for (j in series[i]) { 1584 | if (series[i].hasOwnProperty(j)) { 1585 | copy[j] = series[i][j]; 1586 | } 1587 | } 1588 | newSeries.push(copy) 1589 | } 1590 | return newSeries; 1591 | } 1592 | 1593 | function processSeries(chart, keyType) { 1594 | var i; 1595 | 1596 | var opts = chart.options; 1597 | var series = chart.rawData; 1598 | 1599 | // see if one series or multiple 1600 | if (!isArray(series) || typeof series[0] !== "object" || isArray(series[0])) { 1601 | series = [{name: opts.label || "Value", data: series}]; 1602 | chart.hideLegend = true; 1603 | } else { 1604 | chart.hideLegend = false; 1605 | } 1606 | if ((opts.discrete === null || opts.discrete === undefined) && keyType !== "bubble" && keyType !== "number") { 1607 | chart.discrete = detectDiscrete(series); 1608 | } else { 1609 | chart.discrete = opts.discrete; 1610 | } 1611 | if (chart.discrete) { 1612 | keyType = "string"; 1613 | } 1614 | if (chart.options.xtype) { 1615 | keyType = chart.options.xtype; 1616 | } 1617 | 1618 | // right format 1619 | series = copySeries(series); 1620 | for (i = 0; i < series.length; i++) { 1621 | series[i].data = formatSeriesData(toArr(series[i].data), keyType); 1622 | } 1623 | 1624 | return series; 1625 | } 1626 | 1627 | function processSimple(chart) { 1628 | var perfectData = toArr(chart.rawData), i; 1629 | for (i = 0; i < perfectData.length; i++) { 1630 | perfectData[i] = [toStr(perfectData[i][0]), toFloat(perfectData[i][1])]; 1631 | } 1632 | return perfectData; 1633 | } 1634 | 1635 | function processTime(chart) 1636 | { 1637 | var i, data = chart.rawData; 1638 | for (i = 0; i < data.length; i++) { 1639 | data[i][1] = toDate(data[i][1]); 1640 | data[i][2] = toDate(data[i][2]); 1641 | } 1642 | return data; 1643 | } 1644 | 1645 | function processLineData(chart) { 1646 | return processSeries(chart, "datetime"); 1647 | } 1648 | 1649 | function processColumnData(chart) { 1650 | return processSeries(chart, "string"); 1651 | } 1652 | 1653 | function processBarData(chart) { 1654 | return processSeries(chart, "string"); 1655 | } 1656 | 1657 | function processAreaData(chart) { 1658 | return processSeries(chart, "datetime"); 1659 | } 1660 | 1661 | function processScatterData(chart) { 1662 | return processSeries(chart, "number"); 1663 | } 1664 | 1665 | function processBubbleData(chart) { 1666 | return processSeries(chart, "bubble"); 1667 | } 1668 | 1669 | function createChart(chartType, chart, element, dataSource, opts, processData) { 1670 | var elementId; 1671 | if (typeof element === "string") { 1672 | elementId = element; 1673 | element = document.getElementById(element); 1674 | if (!element) { 1675 | throw new Error("No element with id " + elementId); 1676 | } 1677 | } 1678 | 1679 | chart.element = element; 1680 | opts = merge(Chartkick.options, opts || {}); 1681 | chart.options = opts; 1682 | chart.dataSource = dataSource; 1683 | 1684 | if (!processData) { 1685 | processData = function (chart) { 1686 | return chart.rawData; 1687 | } 1688 | } 1689 | 1690 | // getters 1691 | chart.getElement = function () { 1692 | return element; 1693 | }; 1694 | chart.getDataSource = function () { 1695 | return chart.dataSource; 1696 | }; 1697 | chart.getData = function () { 1698 | return chart.data; 1699 | }; 1700 | chart.getOptions = function () { 1701 | return chart.options; 1702 | }; 1703 | chart.getChartObject = function () { 1704 | return chart.chart; 1705 | }; 1706 | chart.getAdapter = function () { 1707 | return chart.adapter; 1708 | }; 1709 | 1710 | var callback = function () { 1711 | chart.data = processData(chart); 1712 | renderChart(chartType, chart); 1713 | }; 1714 | 1715 | // functions 1716 | chart.updateData = function (dataSource, options) { 1717 | chart.dataSource = dataSource; 1718 | if (options) { 1719 | chart.options = merge(Chartkick.options, options); 1720 | } 1721 | fetchDataSource(chart, callback, dataSource); 1722 | }; 1723 | chart.setOptions = function (options) { 1724 | chart.options = merge(Chartkick.options, options); 1725 | chart.redraw(); 1726 | }; 1727 | chart.redraw = function() { 1728 | fetchDataSource(chart, callback, chart.rawData); 1729 | }; 1730 | chart.refreshData = function () { 1731 | if (typeof chart.dataSource === "string") { 1732 | // prevent browser from caching 1733 | var sep = chart.dataSource.indexOf("?") === -1 ? "?" : "&"; 1734 | var url = chart.dataSource + sep + "_=" + (new Date()).getTime(); 1735 | fetchDataSource(chart, callback, url); 1736 | } 1737 | }; 1738 | chart.stopRefresh = function () { 1739 | if (chart.intervalId) { 1740 | clearInterval(chart.intervalId); 1741 | } 1742 | }; 1743 | chart.toImage = function () { 1744 | if (chart.adapter === "chartjs") { 1745 | return chart.chart.toBase64Image(); 1746 | } else { 1747 | return null; 1748 | } 1749 | } 1750 | 1751 | Chartkick.charts[element.id] = chart; 1752 | 1753 | fetchDataSource(chart, callback, dataSource); 1754 | 1755 | if (opts.refresh) { 1756 | chart.intervalId = setInterval( function () { 1757 | chart.refreshData(); 1758 | }, opts.refresh * 1000); 1759 | } 1760 | } 1761 | 1762 | // define classes 1763 | 1764 | Chartkick = { 1765 | LineChart: function (element, dataSource, options) { 1766 | createChart("LineChart", this, element, dataSource, options, processLineData); 1767 | }, 1768 | PieChart: function (element, dataSource, options) { 1769 | createChart("PieChart", this, element, dataSource, options, processSimple); 1770 | }, 1771 | ColumnChart: function (element, dataSource, options) { 1772 | createChart("ColumnChart", this, element, dataSource, options, processColumnData); 1773 | }, 1774 | BarChart: function (element, dataSource, options) { 1775 | createChart("BarChart", this, element, dataSource, options, processBarData); 1776 | }, 1777 | AreaChart: function (element, dataSource, options) { 1778 | createChart("AreaChart", this, element, dataSource, options, processAreaData); 1779 | }, 1780 | GeoChart: function (element, dataSource, options) { 1781 | createChart("GeoChart", this, element, dataSource, options, processSimple); 1782 | }, 1783 | ScatterChart: function (element, dataSource, options) { 1784 | createChart("ScatterChart", this, element, dataSource, options, processScatterData); 1785 | }, 1786 | BubbleChart: function (element, dataSource, options) { 1787 | createChart("BubbleChart", this, element, dataSource, options, processBubbleData); 1788 | }, 1789 | Timeline: function (element, dataSource, options) { 1790 | createChart("Timeline", this, element, dataSource, options, processTime); 1791 | }, 1792 | charts: {}, 1793 | configure: function (options) { 1794 | for (var key in options) { 1795 | if (options.hasOwnProperty(key)) { 1796 | config[key] = options[key]; 1797 | } 1798 | } 1799 | }, 1800 | eachChart: function (callback) { 1801 | for (var chartId in Chartkick.charts) { 1802 | if (Chartkick.charts.hasOwnProperty(chartId)) { 1803 | callback(Chartkick.charts[chartId]); 1804 | } 1805 | } 1806 | }, 1807 | options: {}, 1808 | adapters: adapters, 1809 | createChart: createChart 1810 | }; 1811 | 1812 | if (typeof module === "object" && typeof module.exports === "object") { 1813 | module.exports = Chartkick; 1814 | } else { 1815 | window.Chartkick = Chartkick; 1816 | } 1817 | }(window)); 1818 | --------------------------------------------------------------------------------