├── 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 |
--------------------------------------------------------------------------------