chartnado 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3a2deb50132fdc707f83201e12dddf65b9d55de5
4
+ data.tar.gz: 3875cfbbacb92a1fa6d3ac486abfa5f587a845f7
5
+ SHA512:
6
+ metadata.gz: 7b84dcfcff0a8df214eb2e8bd8c223eede3251e76764e34cce6bb9a5e738e9d029c517fac121e1615889b095c96e776b3aa8640d48c62de1d9b7ccfba31ee459
7
+ data.tar.gz: 3d2944100f2ec9022764e004f12301489e951542ac7b02cd8cd145db6b1fdfb781ffa2fff52df9e203bc79c343bdc3aa964501871aaa60a003307f041108ad05
@@ -0,0 +1,2 @@
1
+ bump:
2
+ tag: true
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .ruby*
7
+ .idea*
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
20
+ Gemfile-custom
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ - 2.0.0
5
+ # uncomment this line if your project needs to run something other than `rake`:
6
+ # script: bundle exec rspec spec
7
+
8
+ script:
9
+ CODECLIMATE_REPO_TOKEN= bundle exec rake
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in chartkick.gemspec
4
+ gemspec
5
+
6
+ eval File.read('Gemfile-custom') if File.exist?('Gemfile-custom')
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2014 Andrew Brown
2
+ Original chartkick javasript copyright (c) Andrew Kane
3
+
4
+ MIT License
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,137 @@
1
+ # Chartnado [![Gem Version](https://badge.fury.io/rb/chartnado.svg)](http://badge.fury.io/rb/chartnado) [![Travis CI Status](https://travis-ci.org/dontfidget/chartnado.png?branch=master)](https://travis-ci.org/dontfidget/chartnado) [![Code Climate](https://codeclimate.com/github/dontfidget/chartnado.png)](https://codeclimate.com/github/dontfidget/chartnado) [![Code Climate](https://codeclimate.com/github/dontfidget/chartnado/coverage.png)](https://codeclimate.com/github/dontfidget/chartnado) [![Dependency Status](https://gemnasium.com/dontfidget/chartnado.svg)](https://gemnasium.com/dontfidget/chartnado)
2
+
3
+ Chartnado layers on top of [`chartkick`](http://ankane.github.io/chartkick/) and [`chartkick-remote`](http://github.com/dontfidget/chartkick-remote) allowing basic vector-style operations directly on to make it easy to feed them into charts. It also provides some useful defaults and the ability to show totals on pie and stacked area charts when using google charts.
4
+
5
+ ## Usage
6
+
7
+ In your controller, add the following to tell the controller to respond to json requests for chart data:
8
+
9
+ ```ruby
10
+ include Chartnado
11
+ ```
12
+
13
+ Then in your views, now in your views, you can write an expression to show the average tasks completed per today relative to total tasks:
14
+
15
+ ```ruby
16
+ <%= line_chart { Task.group_by_day(:completed_at).count / Task.count } %>
17
+ ```
18
+
19
+ ## Totals
20
+
21
+ By default chartnado adds totals to pie and stacked area charts using some hacky settings for google charts. To get the total to appear, you need to use the chartnado version of the chartkick javascript called `chartkick-chartnado.js` instead of `chartkick.js`. If you are including the javascript in sprockets manifest file, this:
22
+
23
+ ```
24
+ //= require chartkick
25
+ ```
26
+
27
+ should be replaced by this:
28
+
29
+ ```
30
+ //= require chartkick-chartnado
31
+ ```
32
+
33
+ ## Supported Vector Operations on Series
34
+
35
+ Chartnado supports the following operations on series/multiple-series data:
36
+
37
+ * Single/Multiple-Series * Scalar
38
+ * Single/Multiple-Series / Scalar
39
+ * Single/Multiple-Series / Single Series
40
+ * Single Series / Single Series
41
+ * Multiple-Series / Single Series
42
+ * Multiple-Series / Multiple Series
43
+ * Single/Multiple-Series + Scalar
44
+
45
+ A "Series" is a hash of values (i.e. `{ 2 => 4, 3 => 9 }`). A "Multiple-Series" can either be specified in two ways:
46
+
47
+ 1. With the series identifier as the first element in each array that forms the key, as in:
48
+ ```ruby
49
+ {['series a', 0] => 1}, ['series b', 0] => 2}
50
+ ```
51
+
52
+ 1. With the series identifier as the first element in an array of single series, as in:
53
+ ```ruby
54
+ [['series a', {0 => 1}], ['series b', {0 => 2}]]
55
+ ```
56
+
57
+ All series in an operation must use the same format.
58
+
59
+ ## Remote Requests
60
+
61
+ By default requests for data in blocks, will be fetched remotely, unless `remote: false` is passed as an option to *chartkick_remote*. Using this methodology, it's easy to write a page that makes many, many json requests, which may swamp your server and possibly even time out if you have a global `timeout` value set for your ajax requests. @maccman's jquery.ajax.queue.coffee script provides a basic queueing transport layer for ajax requests which I've modified to provide an option to set the maximum number of requests that can be made in parallel (see https://gist.github.com/dontfidget/1ad9ab33971b64fe6fef). This is provided as part of Chartnado and you can include it in your javascript manifest like this:
62
+
63
+ ```
64
+ //= require jquery.ajax.queue-concurrent
65
+ ```
66
+
67
+ Chartnado extends chartkick to accept an *ajaxOptions* hash, which can be passed via chartkick_remote, which means you can then specify the maximum number of allowable requests globally for your page as follows:
68
+
69
+
70
+ ```
71
+ chartkick_remote ajaxOptions: {queue: true, queueMaxConcurrency: 2}
72
+ ```
73
+
74
+ ## Chartnado::SeriesHelper
75
+
76
+ Chartnado also offers direct access to the helpers that implement the above operators.
77
+
78
+ * series_product
79
+ * series_ratio
80
+ * series_sum
81
+
82
+ To include these, just add:
83
+
84
+ `include Chartnado::Series`
85
+
86
+ ### group_by
87
+
88
+ While you can use ActiveRequest::Query.group to group results, you may find it useful to (a) make the grouping the first key, and (b) aggregate/rename groups. `group_by` provides this ability as follows:
89
+
90
+ ```ruby
91
+ group_by('owners.id', Task.group_by_day(:completed_at)) { count }
92
+
93
+ ```
94
+
95
+ You can call it as:
96
+
97
+ ```ruby
98
+ group_by(<expression>, scope, optional_label_block, &eval_block)
99
+ ```
100
+
101
+ where *block* calls the aggregating function you want applied to scope and *optional_label_block* is passed each key and data, so you can change the key (and even the data if you like). The result for hash entries with identical keys is summed. The label block is expected to return a 2-element array, where the first element is the key and the second element is the data.
102
+
103
+ To include these, just add:
104
+
105
+ `include Chartnado::GroupBy`
106
+
107
+ ### Defining series
108
+
109
+ It may be useful to define series/multiple-series inside your code so that it can be shared in multiple views. Chartnado provides the `define_series` and `define_multiple_series` class methods to aid in adding shared series in your helpers.
110
+
111
+ ```ruby
112
+ # for a single series
113
+ define_series(:my_series) { { 0 => 1 } / 2 }`
114
+
115
+ # for multiple series
116
+ define_multiple_series(
117
+ my_first_series: -> { { 0 => 1 } / 2 }
118
+ my_second_series: -> { { 0 => 1 } / 2 }
119
+ )
120
+ ```
121
+
122
+ To include these methods in a view helper, just add the following to the helper:
123
+
124
+ `include Chartnado::Helpers::SeriesHelpers
125
+
126
+ ### Wrapping the chart renderer
127
+
128
+ You can wrap the chart rendering method in your controller if you want finer control over the rendering process, such as wrapping the chartkick output in a partial. To do this, include something like the following in your controller:
129
+
130
+ ```ruby
131
+ chartnado_wrapper :custom_renderer
132
+
133
+ def custom_renderer(*args, **options, &block)
134
+ title = options[:title]
135
+ render 'my-chart-partial', title: title, &block
136
+ end
137
+ ```
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'chartnado/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "chartnado"
8
+ spec.version = Chartnado::VERSION
9
+ spec.authors = ["Andrew S. Brown"]
10
+ spec.email = ["andrew@dontfidget.com"]
11
+ spec.description = %q{Chartkick charts with extras}
12
+ spec.summary = %q{Chartkick charts with extras}
13
+ spec.homepage = "https://github.com/dontfidget/chartnado"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
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
+ spec.required_ruby_version = '>= 2.0'
21
+
22
+ spec.add_dependency "activesupport", '>= 3'
23
+ spec.add_dependency "chartkick", '>= 1.0'
24
+ spec.add_dependency "chartkick-remote", '>= 1.0'
25
+ spec.add_dependency "railties", ">= 3.1"
26
+ spec.add_development_dependency "bundler", '~> 1.3'
27
+ spec.add_development_dependency "rake", '~> 10.3'
28
+ spec.add_development_dependency "rspec", '~> 3.0'
29
+ spec.add_development_dependency "rspec-core", '~> 3.0'
30
+ spec.add_development_dependency "rspec-mocks", '~> 3.0'
31
+ spec.add_development_dependency "rspec-rails", '~> 3.0'
32
+ spec.add_development_dependency "travis-lint", '~> 1.8'
33
+ spec.add_development_dependency "codeclimate-test-reporter", '~> 0.3'
34
+ # spec.add_development_dependency "rspec-html-matchers", '~> 0.6.1', '>= 0.6.1'
35
+ end
@@ -0,0 +1,864 @@
1
+ /*
2
+ * Derived from:
3
+ *
4
+ * Chartkick.js
5
+ * Create beautiful Javascript charts with minimal code
6
+ * https://github.com/ankane/chartkick.js
7
+ * v1.2.2
8
+ * MIT License
9
+ */
10
+
11
+ /*jslint browser: true, indent: 2, plusplus: true, vars: true */
12
+
13
+ (function (window) {
14
+ 'use strict';
15
+
16
+ var config = window.Chartkick || {};
17
+ var Chartkick, ISO8601_PATTERN, DECIMAL_SEPARATOR, adapters = [];
18
+
19
+ // helpers
20
+
21
+ function isArray(variable) {
22
+ return Object.prototype.toString.call(variable) === "[object Array]";
23
+ }
24
+
25
+ function isFunction(variable) {
26
+ return variable instanceof Function;
27
+ }
28
+
29
+ function isPlainObject(variable) {
30
+ return !isFunction(variable) && variable instanceof Object;
31
+ }
32
+
33
+ // https://github.com/madrobby/zepto/blob/master/src/zepto.js
34
+ function extend(target, source) {
35
+ var key;
36
+ for (key in source) {
37
+ if (isPlainObject(source[key]) || isArray(source[key])) {
38
+ if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
39
+ target[key] = {};
40
+ }
41
+ if (isArray(source[key]) && !isArray(target[key])) {
42
+ target[key] = [];
43
+ }
44
+ extend(target[key], source[key]);
45
+ } else if (source[key] !== undefined) {
46
+ target[key] = source[key];
47
+ }
48
+ }
49
+ }
50
+
51
+ function merge(obj1, obj2) {
52
+ var target = {};
53
+ extend(target, obj1);
54
+ extend(target, obj2);
55
+ return target;
56
+ }
57
+
58
+ // https://github.com/Do/iso8601.js
59
+ ISO8601_PATTERN = /(\d\d\d\d)(\-)?(\d\d)(\-)?(\d\d)(T)?(\d\d)(:)?(\d\d)?(:)?(\d\d)?([\.,]\d+)?($|Z|([\+\-])(\d\d)(:)?(\d\d)?)/i;
60
+ DECIMAL_SEPARATOR = String(1.5).charAt(1);
61
+
62
+ function parseISO8601(input) {
63
+ var day, hour, matches, milliseconds, minutes, month, offset, result, seconds, type, year;
64
+ type = Object.prototype.toString.call(input);
65
+ if (type === '[object Date]') {
66
+ return input;
67
+ }
68
+ if (type !== '[object String]') {
69
+ return;
70
+ }
71
+ if (matches = input.match(ISO8601_PATTERN)) {
72
+ year = parseInt(matches[1], 10);
73
+ month = parseInt(matches[3], 10) - 1;
74
+ day = parseInt(matches[5], 10);
75
+ hour = parseInt(matches[7], 10);
76
+ minutes = matches[9] ? parseInt(matches[9], 10) : 0;
77
+ seconds = matches[11] ? parseInt(matches[11], 10) : 0;
78
+ milliseconds = matches[12] ? parseFloat(DECIMAL_SEPARATOR + matches[12].slice(1)) * 1000 : 0;
79
+ result = Date.UTC(year, month, day, hour, minutes, seconds, milliseconds);
80
+ if (matches[13] && matches[14]) {
81
+ offset = matches[15] * 60;
82
+ if (matches[17]) {
83
+ offset += parseInt(matches[17], 10);
84
+ }
85
+ offset *= matches[14] === '-' ? -1 : 1;
86
+ result -= offset * 60 * 1000;
87
+ }
88
+ return new Date(result);
89
+ }
90
+ }
91
+ // end iso8601.js
92
+
93
+ function negativeValues(series) {
94
+ var i, j, data;
95
+ for (i = 0; i < series.length; i++) {
96
+ data = series[i].data;
97
+ for (j = 0; j < data.length; j++) {
98
+ if (data[j][1] < 0) {
99
+ return true;
100
+ }
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+
106
+ function jsOptionsFunc(defaultOptions, hideLegend, setMin, setMax, setStacked) {
107
+ return function (series, opts, chartOptions) {
108
+ var options = merge({}, defaultOptions);
109
+ options = merge(options, chartOptions || {});
110
+
111
+ // hide legend
112
+ // this is *not* an external option!
113
+ if (opts.hideLegend) {
114
+ hideLegend(options);
115
+ }
116
+
117
+ // min
118
+ if ("min" in opts) {
119
+ setMin(options, opts.min);
120
+ } else if (!negativeValues(series)) {
121
+ setMin(options, 0);
122
+ }
123
+
124
+ // max
125
+ if ("max" in opts) {
126
+ setMax(options, opts.max);
127
+ }
128
+
129
+ if (opts.stacked) {
130
+ setStacked(options);
131
+ }
132
+
133
+ if (opts.colors) {
134
+ options.colors = opts.colors;
135
+ }
136
+
137
+ // merge library last
138
+ options = merge(options, opts.library || {});
139
+
140
+ return options;
141
+ };
142
+ }
143
+
144
+ function setText(element, text) {
145
+ if (document.body.innerText) {
146
+ element.innerText = text;
147
+ } else {
148
+ element.textContent = text;
149
+ }
150
+ }
151
+
152
+ function chartError(element, message) {
153
+ setText(element, "Error Loading Chart: " + message);
154
+ element.style.color = "#ff0000";
155
+ }
156
+
157
+ function getJSON(element, url, options, success) {
158
+ var $ = window.jQuery || window.Zepto || window.$;
159
+ $.ajax($.extend({}, options, {
160
+ dataType: "json",
161
+ url: url,
162
+ success: success,
163
+ error: function (jqXHR, textStatus, errorThrown) {
164
+ var message = (typeof errorThrown === "string") ? errorThrown : errorThrown.message;
165
+ chartError(element, message);
166
+ }
167
+ }));
168
+ }
169
+
170
+ function errorCatcher(chart, callback) {
171
+ try {
172
+ callback(chart);
173
+ } catch (err) {
174
+ chartError(chart.element, err.message);
175
+ throw err;
176
+ }
177
+ }
178
+
179
+ function fetchDataSource(chart, callback) {
180
+ if (typeof chart.dataSource === "string") {
181
+ getJSON(chart.element, chart.dataSource, chart.options.ajaxOptions, function (data, textStatus, jqXHR) {
182
+ chart.data = data;
183
+ errorCatcher(chart, callback);
184
+ });
185
+ } else {
186
+ chart.data = chart.dataSource;
187
+ errorCatcher(chart, callback);
188
+ }
189
+ }
190
+
191
+ // type conversions
192
+
193
+ function toStr(n) {
194
+ return "" + n;
195
+ }
196
+
197
+ function toFloat(n) {
198
+ return parseFloat(n);
199
+ }
200
+
201
+ function toDate(n) {
202
+ if (typeof n !== "object") {
203
+ if (typeof n === "number") {
204
+ n = new Date(n * 1000); // ms
205
+ } else { // str
206
+ // try our best to get the str into iso8601
207
+ // TODO be smarter about this
208
+ var str = n.replace(/ /, "T").replace(" ", "").replace("UTC", "Z");
209
+ n = parseISO8601(str) || new Date(n);
210
+ }
211
+ }
212
+ return n;
213
+ }
214
+
215
+ function toArr(n) {
216
+ if (!isArray(n)) {
217
+ var arr = [], i;
218
+ for (i in n) {
219
+ if (n.hasOwnProperty(i)) {
220
+ arr.push([i, n[i]]);
221
+ }
222
+ }
223
+ n = arr;
224
+ }
225
+ return n;
226
+ }
227
+
228
+ function sortByTime(a, b) {
229
+ return a[0].getTime() - b[0].getTime();
230
+ }
231
+
232
+ if ("Highcharts" in window) {
233
+ var HighchartsAdapter = new function () {
234
+ var Highcharts = window.Highcharts;
235
+
236
+ var defaultOptions = {
237
+ chart: {},
238
+ xAxis: {
239
+ labels: {
240
+ style: {
241
+ fontSize: "12px"
242
+ }
243
+ }
244
+ },
245
+ yAxis: {
246
+ title: {
247
+ text: null
248
+ },
249
+ labels: {
250
+ style: {
251
+ fontSize: "12px"
252
+ }
253
+ }
254
+ },
255
+ title: {
256
+ text: null
257
+ },
258
+ credits: {
259
+ enabled: false
260
+ },
261
+ legend: {
262
+ borderWidth: 0
263
+ },
264
+ tooltip: {
265
+ style: {
266
+ fontSize: "12px"
267
+ }
268
+ },
269
+ plotOptions: {
270
+ areaspline: {},
271
+ series: {
272
+ marker: {}
273
+ }
274
+ }
275
+ };
276
+
277
+ var hideLegend = function (options) {
278
+ options.legend.enabled = false;
279
+ };
280
+
281
+ var setMin = function (options, min) {
282
+ options.yAxis.min = min;
283
+ };
284
+
285
+ var setMax = function (options, max) {
286
+ options.yAxis.max = max;
287
+ };
288
+
289
+ var setStacked = function (options) {
290
+ options.plotOptions.series.stacking = "normal";
291
+ };
292
+
293
+ var jsOptions = jsOptionsFunc(defaultOptions, hideLegend, setMin, setMax, setStacked);
294
+
295
+ this.renderLineChart = function (chart, chartType) {
296
+ chartType = chartType || "spline";
297
+ var chartOptions = {};
298
+ if (chartType === "areaspline") {
299
+ chartOptions = {
300
+ plotOptions: {
301
+ areaspline: {
302
+ stacking: "normal"
303
+ },
304
+ series: {
305
+ marker: {
306
+ enabled: false
307
+ }
308
+ }
309
+ }
310
+ };
311
+ }
312
+ var options = jsOptions(chart.data, chart.options, chartOptions), data, i, j;
313
+ options.xAxis.type = chart.options.discrete ? "category" : "datetime";
314
+ options.chart.type = chartType;
315
+ options.chart.renderTo = chart.element.id;
316
+
317
+ var series = chart.data;
318
+ for (i = 0; i < series.length; i++) {
319
+ data = series[i].data;
320
+ if (!chart.options.discrete) {
321
+ for (j = 0; j < data.length; j++) {
322
+ data[j][0] = data[j][0].getTime();
323
+ }
324
+ }
325
+ series[i].marker = {symbol: "circle"};
326
+ }
327
+ options.series = series;
328
+ new Highcharts.Chart(options);
329
+ };
330
+
331
+ this.renderPieChart = function (chart) {
332
+ var chartOptions = {};
333
+ if (chart.options.colors) {
334
+ chartOptions.colors = chart.options.colors;
335
+ }
336
+ var options = merge(merge(defaultOptions, chartOptions), chart.options.library || {});
337
+ options.chart.renderTo = chart.element.id;
338
+ options.series = [{
339
+ type: "pie",
340
+ name: "Value",
341
+ data: chart.data
342
+ }];
343
+ new Highcharts.Chart(options);
344
+ };
345
+
346
+ this.renderColumnChart = function (chart, chartType) {
347
+ var chartType = chartType || "column";
348
+ var series = chart.data;
349
+ var options = jsOptions(series, chart.options), i, j, s, d, rows = [];
350
+ options.chart.type = chartType;
351
+ options.chart.renderTo = chart.element.id;
352
+
353
+ for (i = 0; i < series.length; i++) {
354
+ s = series[i];
355
+
356
+ for (j = 0; j < s.data.length; j++) {
357
+ d = s.data[j];
358
+ if (!rows[d[0]]) {
359
+ rows[d[0]] = new Array(series.length);
360
+ }
361
+ rows[d[0]][i] = d[1];
362
+ }
363
+ }
364
+
365
+ var categories = [];
366
+ for (i in rows) {
367
+ if (rows.hasOwnProperty(i)) {
368
+ categories.push(i);
369
+ }
370
+ }
371
+ options.xAxis.categories = categories;
372
+
373
+ var newSeries = [];
374
+ for (i = 0; i < series.length; i++) {
375
+ d = [];
376
+ for (j = 0; j < categories.length; j++) {
377
+ d.push(rows[categories[j]][i] || 0);
378
+ }
379
+
380
+ newSeries.push({
381
+ name: series[i].name,
382
+ data: d
383
+ });
384
+ }
385
+ options.series = newSeries;
386
+
387
+ new Highcharts.Chart(options);
388
+ };
389
+
390
+ var self = this;
391
+
392
+ this.renderBarChart = function (chart) {
393
+ self.renderColumnChart(chart, "bar");
394
+ };
395
+
396
+ this.renderAreaChart = function (chart) {
397
+ self.renderLineChart(chart, "areaspline");
398
+ };
399
+ };
400
+ adapters.push(HighchartsAdapter);
401
+ }
402
+ if (window.google && window.google.setOnLoadCallback) {
403
+ var GoogleChartsAdapter = new function () {
404
+ var google = window.google;
405
+
406
+ var loaded = {};
407
+ var callbacks = [];
408
+
409
+ var runCallbacks = function () {
410
+ var cb, call;
411
+ for (var i = 0; i < callbacks.length; i++) {
412
+ cb = callbacks[i];
413
+ call = google.visualization && ((cb.pack == "corechart" && google.visualization.LineChart) || (cb.pack == "timeline" && google.visualization.Timeline))
414
+ if (call) {
415
+ cb.callback();
416
+ callbacks.splice(i, 1);
417
+ i--;
418
+ }
419
+ }
420
+ };
421
+
422
+ var waitForLoaded = function (pack, callback) {
423
+ if (!callback) {
424
+ callback = pack;
425
+ pack = "corechart";
426
+ }
427
+
428
+ callbacks.push({pack: pack, callback: callback});
429
+
430
+ if (loaded[pack]) {
431
+ runCallbacks();
432
+ } else {
433
+ loaded[pack] = true;
434
+
435
+ // https://groups.google.com/forum/#!topic/google-visualization-api/fMKJcyA2yyI
436
+ var loadOptions = {
437
+ packages: [pack],
438
+ callback: runCallbacks
439
+ };
440
+ if (config.language) {
441
+ loadOptions.language = config.language;
442
+ }
443
+ google.load("visualization", "1", loadOptions);
444
+ }
445
+ };
446
+
447
+ // Set chart options
448
+ var defaultOptions = {
449
+ chartArea: {},
450
+ fontName: "'Lucida Grande', 'Lucida Sans Unicode', Verdana, Arial, Helvetica, sans-serif",
451
+ pointSize: 6,
452
+ legend: {
453
+ textStyle: {
454
+ fontSize: 12,
455
+ color: "#444"
456
+ },
457
+ alignment: "center",
458
+ position: "right"
459
+ },
460
+ curveType: "function",
461
+ hAxis: {
462
+ textStyle: {
463
+ color: "#666",
464
+ fontSize: 12
465
+ },
466
+ gridlines: {
467
+ color: "transparent"
468
+ },
469
+ baselineColor: "#ccc",
470
+ viewWindow: {}
471
+ },
472
+ vAxis: {
473
+ textStyle: {
474
+ color: "#666",
475
+ fontSize: 12
476
+ },
477
+ baselineColor: "#ccc",
478
+ viewWindow: {}
479
+ },
480
+ tooltip: {
481
+ textStyle: {
482
+ color: "#666",
483
+ fontSize: 12
484
+ }
485
+ }
486
+ };
487
+
488
+ var hideLegend = function (options) {
489
+ options.legend.position = "none";
490
+ };
491
+
492
+ var setMin = function (options, min) {
493
+ options.vAxis.viewWindow.min = min;
494
+ };
495
+
496
+ var setMax = function (options, max) {
497
+ options.vAxis.viewWindow.max = max;
498
+ };
499
+
500
+ var setBarMin = function (options, min) {
501
+ options.hAxis.viewWindow.min = min;
502
+ };
503
+
504
+ var setBarMax = function (options, max) {
505
+ options.hAxis.viewWindow.max = max;
506
+ };
507
+
508
+ var setStacked = function (options) {
509
+ options.isStacked = true;
510
+ };
511
+
512
+ var jsOptions = jsOptionsFunc(defaultOptions, hideLegend, setMin, setMax, setStacked);
513
+
514
+ // cant use object as key
515
+ var createDataTable = function (series, columnType) {
516
+ var data = new google.visualization.DataTable();
517
+ data.addColumn(columnType, "");
518
+
519
+ var i, j, s, d, rows = [], pos, rowPos = [], columns;
520
+ for (i = 0, columns = 0; i < series.length; i++, columns++) {
521
+ rowPos[i] = columns;
522
+ if (series[i].tooltip) {
523
+ columns++;
524
+ }
525
+ }
526
+
527
+ function setData(key, value, pos, valueIsString) {
528
+ var newKey = (columnType === "datetime") ? key.getTime() : key;
529
+ if (!rows[newKey]) {
530
+ rows[newKey] = new Array(columns);
531
+ }
532
+ if (!valueIsString) {
533
+ value = toFloat(value);
534
+ }
535
+ rows[newKey][pos] = value;
536
+ }
537
+
538
+ for (i = 0; i < series.length; i++) {
539
+ pos = rowPos[i];
540
+ s = series[i];
541
+ data.addColumn("number", s.name);
542
+ for (j = 0; j < s.data.length; j++) {
543
+ d = s.data[j];
544
+ setData(d[0], toFloat(d[1]), pos);
545
+ }
546
+ if (s.tooltip) {
547
+ data.addColumn({type: 'string', role: 'tooltip'});
548
+ for (j = 0; j < s.tooltip.length; j++) {
549
+ d = s.tooltip[j];
550
+ setData(d[0], d[1], pos + 1, true);
551
+ }
552
+ }
553
+ }
554
+
555
+ var rows2 = [];
556
+ for (i in rows) {
557
+ if (rows.hasOwnProperty(i)) {
558
+ rows2.push([(columnType === "datetime") ? new Date(toFloat(i)) : i].concat(rows[i]));
559
+ }
560
+ }
561
+ if (columnType === "datetime") {
562
+ rows2.sort(sortByTime);
563
+ }
564
+ data.addRows(rows2);
565
+
566
+ return data;
567
+ };
568
+
569
+ var resize = function (callback) {
570
+ if (window.attachEvent) {
571
+ window.attachEvent("onresize", callback);
572
+ } else if (window.addEventListener) {
573
+ window.addEventListener("resize", callback, true);
574
+ }
575
+ callback();
576
+ };
577
+
578
+ this.renderLineChart = function (chart) {
579
+ waitForLoaded(function () {
580
+ var options = jsOptions(chart.data, chart.options);
581
+ var data = createDataTable(chart.data, chart.options.discrete ? "string" : "datetime");
582
+ chart.chart = new google.visualization.LineChart(chart.element);
583
+ resize(function () {
584
+ chart.chart.draw(data, options);
585
+ });
586
+ });
587
+ };
588
+
589
+ this.renderPieChart = function (chart) {
590
+ waitForLoaded(function () {
591
+ var chartOptions = {
592
+ chartArea: {
593
+ top: "10%",
594
+ height: "80%"
595
+ }
596
+ };
597
+ if (chart.options.colors) {
598
+ chartOptions.colors = chart.options.colors;
599
+ }
600
+ var options = merge(merge(defaultOptions, chartOptions), chart.options.library || {});
601
+
602
+ var data = new google.visualization.DataTable();
603
+ data.addColumn("string", "");
604
+ data.addColumn("number", "Value");
605
+ data.addRows(chart.data);
606
+
607
+ chart.chart = new google.visualization.PieChart(chart.element);
608
+ resize(function () {
609
+ chart.chart.draw(data, options);
610
+ });
611
+ });
612
+ };
613
+
614
+ this.renderColumnChart = function (chart) {
615
+ waitForLoaded(function () {
616
+ var options = jsOptions(chart.data, chart.options);
617
+ var data = createDataTable(chart.data, "string");
618
+ chart.chart = new google.visualization.ColumnChart(chart.element);
619
+ resize(function () {
620
+ chart.chart.draw(data, options);
621
+ });
622
+ });
623
+ };
624
+
625
+ this.renderBarChart = function (chart) {
626
+ waitForLoaded(function () {
627
+ var chartOptions = {
628
+ hAxis: {
629
+ gridlines: {
630
+ color: "#ccc"
631
+ }
632
+ }
633
+ };
634
+ var options = jsOptionsFunc(defaultOptions, hideLegend, setBarMin, setBarMax, setStacked)(chart.data, chart.options, chartOptions);
635
+ var data = createDataTable(chart.data, "string");
636
+ chart.chart = new google.visualization.BarChart(chart.element);
637
+ resize(function () {
638
+ chart.chart.draw(data, options);
639
+ });
640
+ });
641
+ };
642
+
643
+ this.renderAreaChart = function (chart) {
644
+ waitForLoaded(function () {
645
+ var chartOptions = {
646
+ isStacked: true,
647
+ pointSize: 0,
648
+ areaOpacity: 0.5
649
+ };
650
+ var options = jsOptions(chart.data, chart.options, chartOptions);
651
+ var data = createDataTable(chart.data, chart.options.discrete ? "string" : "datetime");
652
+ chart.chart = new google.visualization.AreaChart(chart.element);
653
+ resize(function () {
654
+ chart.chart.draw(data, options);
655
+ });
656
+ });
657
+ };
658
+
659
+ this.renderGeoChart = function (chart) {
660
+ waitForLoaded(function () {
661
+ var chartOptions = {
662
+ legend: "none",
663
+ colorAxis: {
664
+ colors: chart.options.colors || ["#f6c7b6", "#ce502d"]
665
+ }
666
+ };
667
+ var options = merge(merge(defaultOptions, chartOptions), chart.options.library || {});
668
+
669
+ var data = new google.visualization.DataTable();
670
+ data.addColumn("string", "");
671
+ data.addColumn("number", "Value");
672
+ data.addRows(chart.data);
673
+
674
+ chart.chart = new google.visualization.GeoChart(chart.element);
675
+ resize(function () {
676
+ chart.chart.draw(data, options);
677
+ });
678
+ });
679
+ };
680
+
681
+ this.renderTimeline = function (chart) {
682
+ waitForLoaded("timeline", function () {
683
+ var chartOptions = {
684
+ legend: "none"
685
+ };
686
+
687
+ if (chart.options.colors) {
688
+ chartOptions.colorAxis.colors = chart.options.colors;
689
+ }
690
+ var options = merge(merge(defaultOptions, chartOptions), chart.options.library || {});
691
+
692
+ var data = new google.visualization.DataTable();
693
+ data.addColumn({type: "string", id: "Name"});
694
+ data.addColumn({type: "date", id: "Start"});
695
+ data.addColumn({type: "date", id: "End"});
696
+ data.addRows(chart.data);
697
+
698
+ chart.chart = new google.visualization.Timeline(chart.element);
699
+
700
+ resize(function () {
701
+ chart.chart.draw(data, options);
702
+ });
703
+ });
704
+ };
705
+ };
706
+
707
+ adapters.push(GoogleChartsAdapter);
708
+ }
709
+
710
+ // TODO add adapter option
711
+ // TODO remove chartType if cross-browser way
712
+ // to get the name of the chart class
713
+ function renderChart(chartType, chart) {
714
+ var i, adapter, fnName;
715
+ fnName = "render" + chartType;
716
+
717
+ for (i = 0; i < adapters.length; i++) {
718
+ adapter = adapters[i];
719
+ if (isFunction(adapter[fnName])) {
720
+ return adapter[fnName](chart);
721
+ }
722
+ }
723
+ throw new Error("No adapter found");
724
+ }
725
+
726
+ // process data
727
+
728
+ function processSeries(series, opts, time) {
729
+ var i, data, key;
730
+
731
+ // see if one series or multiple
732
+ if (!isArray(series) || typeof series[0] !== "object" || isArray(series[0])) {
733
+ series = [{name: "Value", data: series}];
734
+ opts.hideLegend = true;
735
+ } else {
736
+ opts.hideLegend = false;
737
+ }
738
+ if (opts.discrete) {
739
+ time = false;
740
+ }
741
+
742
+ function formatData(inputData, valueIsString) {
743
+ var j, r = [], data = toArr(inputData), value;
744
+ for (j = 0; j < data.length; j++) {
745
+ key = data[j][0];
746
+ key = time ? toDate(key) : toStr(key);
747
+ value = data[j][1];
748
+ if (valueIsString) {
749
+ value = toStr(value)
750
+ } else {
751
+ value = toFloat(value);
752
+ }
753
+ r.push([key, value]);
754
+ }
755
+ if (time) {
756
+ r.sort(sortByTime);
757
+ }
758
+ return r;
759
+ }
760
+
761
+ // right format
762
+ for (i = 0; i < series.length; i++) {
763
+ series[i].data = formatData(series[i].data);
764
+ if (series[i].tooltip) {
765
+ series[i].tooltip = formatData(series[i].tooltip, true);
766
+ }
767
+ }
768
+
769
+ return series;
770
+ }
771
+
772
+ function processSimple(data) {
773
+ var perfectData = toArr(data), i;
774
+ for (i = 0; i < perfectData.length; i++) {
775
+ perfectData[i] = [toStr(perfectData[i][0]), toFloat(perfectData[i][1])];
776
+ }
777
+ return perfectData;
778
+ }
779
+
780
+ function processTime(data)
781
+ {
782
+ var i;
783
+ for (i = 0; i < data.length; i++) {
784
+ data[i][1] = toDate(data[i][1]);
785
+ data[i][2] = toDate(data[i][2]);
786
+ }
787
+ return data;
788
+ }
789
+
790
+ function processLineData(chart) {
791
+ chart.data = processSeries(chart.data, chart.options, true);
792
+ renderChart("LineChart", chart);
793
+ }
794
+
795
+ function processColumnData(chart) {
796
+ chart.data = processSeries(chart.data, chart.options, false);
797
+ renderChart("ColumnChart", chart);
798
+ }
799
+
800
+ function processPieData(chart) {
801
+ chart.data = processSimple(chart.data);
802
+ renderChart("PieChart", chart);
803
+ }
804
+
805
+ function processBarData(chart) {
806
+ chart.data = processSeries(chart.data, chart.options, false);
807
+ renderChart("BarChart", chart);
808
+ }
809
+
810
+ function processAreaData(chart) {
811
+ chart.data = processSeries(chart.data, chart.options, true);
812
+ renderChart("AreaChart", chart);
813
+ }
814
+
815
+ function processGeoData(chart) {
816
+ chart.data = processSimple(chart.data);
817
+ renderChart("GeoChart", chart);
818
+ }
819
+
820
+ function processTimelineData(chart) {
821
+ chart.data = processTime(chart.data);
822
+ renderChart("Timeline", chart);
823
+ }
824
+
825
+ function setElement(chart, element, dataSource, opts, callback) {
826
+ if (typeof element === "string") {
827
+ element = document.getElementById(element);
828
+ }
829
+ chart.element = element;
830
+ chart.options = opts || {};
831
+ chart.dataSource = dataSource;
832
+ Chartkick.charts[element.id] = chart;
833
+ fetchDataSource(chart, callback);
834
+ }
835
+
836
+ // define classes
837
+
838
+ Chartkick = {
839
+ LineChart: function (element, dataSource, opts) {
840
+ setElement(this, element, dataSource, opts, processLineData);
841
+ },
842
+ PieChart: function (element, dataSource, opts) {
843
+ setElement(this, element, dataSource, opts, processPieData);
844
+ },
845
+ ColumnChart: function (element, dataSource, opts) {
846
+ setElement(this, element, dataSource, opts, processColumnData);
847
+ },
848
+ BarChart: function (element, dataSource, opts) {
849
+ setElement(this, element, dataSource, opts, processBarData);
850
+ },
851
+ AreaChart: function (element, dataSource, opts) {
852
+ setElement(this, element, dataSource, opts, processAreaData);
853
+ },
854
+ GeoChart: function (element, dataSource, opts) {
855
+ setElement(this, element, dataSource, opts, processGeoData);
856
+ },
857
+ Timeline: function (element, dataSource, opts) {
858
+ setElement(this, element, dataSource, opts, processTimelineData);
859
+ },
860
+ charts: {}
861
+ };
862
+
863
+ window.Chartkick = Chartkick;
864
+ }(window));