chartnado 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,76 @@
1
+ # from https://gist.github.com/dontfidget/1ad9ab33971b64fe6fef
2
+ # derived from https://gist.github.com/maccman/5790509
3
+
4
+ $ = jQuery
5
+
6
+ queues = {}
7
+
8
+ queue = (name) ->
9
+ name = 'default' if name is true
10
+ queues[name] or= {entries: [], running: 0}
11
+
12
+ next = (name, done) ->
13
+ list = queue(name)
14
+
15
+ if done
16
+ queue(name).running--
17
+
18
+ unless list.entries.length
19
+ return
20
+
21
+ [options, deferred] = list.entries[0]
22
+
23
+ if list.running >= (options.queueMaxConcurrency || 1)
24
+ return
25
+
26
+ list.entries.shift()
27
+
28
+ queue(name).running++
29
+
30
+ $.ajax(options)
31
+ .always(-> next(name, true))
32
+ .done(-> deferred.resolve(arguments...))
33
+ .fail(-> deferred.reject(arguments...))
34
+
35
+ push = (name, options) ->
36
+ list = queue(name)
37
+ deferred = $.Deferred()
38
+
39
+ while options && list.entries.length >= options.queueMaxDepth
40
+ [overflowOptions, overflowDeferred] = list.entries.shift()
41
+ overflowDeferred.reject null, "queue overflow"
42
+
43
+ list.entries.push([options, deferred])
44
+
45
+ next(name)
46
+ deferred.promise()
47
+
48
+ remove = (name, options) ->
49
+ list = queue(name)
50
+
51
+ for [value, _], i in list.entries when value is options
52
+ list.entries.splice(i, 1)
53
+ break
54
+
55
+ $.ajaxTransport '+*', (options) ->
56
+ if options.queue
57
+ queuedOptions = $.extend({}, options)
58
+ queuedOptions.queue = false
59
+ queuedOptions.processData = false
60
+
61
+ send: (headers, complete) ->
62
+ push(options.queue, queuedOptions)
63
+ .done (data, textStatus, jqXHR) ->
64
+ complete(jqXHR.status,
65
+ jqXHR.statusText,
66
+ text: jqXHR.responseText,
67
+ jqXHR.getAllResponseHeaders())
68
+
69
+ .fail (jqXHR, textStatus, errorThrown) ->
70
+ complete(jqXHR.status,
71
+ jqXHR.statusText,
72
+ text: jqXHR.responseText,
73
+ jqXHR.getAllResponseHeaders())
74
+
75
+ abort: ->
76
+ remove(options.queue, queuedOptions)
@@ -0,0 +1,39 @@
1
+ require 'chartnado/version'
2
+ require 'chartnado/series'
3
+ require 'chartnado/group_by'
4
+ require 'chartnado/evaluator'
5
+ require 'chartnado/helpers/chart_helper'
6
+ require 'chartnado/helpers/series_helper'
7
+ require 'chartnado/hash'
8
+ require 'chartkick/remote'
9
+ require 'chartnado/engine' if defined?(Rails)
10
+
11
+ module Chartnado
12
+ extend ActiveSupport::Concern
13
+ attr_accessor :chartnado_options
14
+
15
+ included do
16
+ include Chartkick::Remote
17
+
18
+ helper Chartnado::Helpers::Chart
19
+ end
20
+
21
+ module ClassMethods
22
+ def chartnado_wrapper(wrapper_symbol = nil, **options, &block)
23
+ unless block
24
+ helper_method wrapper_symbol
25
+ block = -> (*args, **options) do
26
+ render_block = args.pop
27
+ send(wrapper_symbol, *args, **options, &render_block)
28
+ end
29
+ end
30
+
31
+ action_filter_options = options.extract!(:only, :except)
32
+
33
+ before_filter action_filter_options do
34
+ self.chartnado_options ||= {}
35
+ self.chartnado_options[:wrapper_proc] = block
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,10 @@
1
+ module Chartnado
2
+ class Engine < ::Rails::Engine
3
+
4
+ initializer "precompile", :group => :all do |app|
5
+ # use a proc instead of a string
6
+ app.config.assets.precompile << Proc.new{|path| path == "chartkick-chartnado.js" }
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,82 @@
1
+ module Chartnado
2
+ class Evaluator < SimpleDelegator
3
+ module Operators
4
+ def self.included(base)
5
+ %i{+ * - /}.each do |method|
6
+ define_method method do |*args, &block|
7
+ if Thread.current[:in_chartnado_block]
8
+ OperatorEvaluator.new(self).send(method, *args)
9
+ elsif defined?(super)
10
+ super(*args, &block)
11
+ else
12
+ raise NoMethodError, "#{method} is not defined"
13
+ end
14
+ end
15
+ end
16
+
17
+ def coerce(other)
18
+ return super unless Thread.current[:in_chartnado_block]
19
+ if other.is_a?(Numeric)
20
+ [self, other]
21
+ else
22
+ super
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ class OperatorEvaluator
29
+ include Series
30
+ extend Forwardable
31
+
32
+ def initialize(object)
33
+ @object = object
34
+ end
35
+
36
+ def *(value)
37
+ without_operators do
38
+ series_product(@object, value)
39
+ end
40
+ end
41
+
42
+ def +(value)
43
+ without_operators do
44
+ series_sum(value, @object)
45
+ end
46
+ end
47
+
48
+ def -(value)
49
+ self + -1.0 * value
50
+ end
51
+
52
+ def /(value)
53
+ without_operators do
54
+ series_ratio(@object, value)
55
+ end
56
+ end
57
+
58
+ def_delegator Chartnado::Evaluator, :without_operators
59
+ end
60
+
61
+ def self.without_operators
62
+ in_chartnado_block = Thread.current[:in_chartnado_block]
63
+ Thread.current[:in_chartnado_block] = nil
64
+ yield
65
+ ensure
66
+ Thread.current[:in_chartnado_block] = in_chartnado_block
67
+ end
68
+
69
+ def self.with_operators(&block)
70
+ original_value = Thread.current[:in_chartnado_block]
71
+ binding = eval 'self', block.binding
72
+ Thread.current[:in_chartnado_block] = true
73
+ Evaluator.new(binding).instance_eval(&block)
74
+ ensure
75
+ Thread.current[:in_chartnado_block] = original_value
76
+ end
77
+ end
78
+
79
+ def self.with_chartnado_dsl(&block)
80
+ Evaluator.with_operators(&block)
81
+ end
82
+ end
@@ -0,0 +1,92 @@
1
+ require 'chartnado/series'
2
+
3
+ module Chartnado::GroupBy
4
+ include Chartnado::Series
5
+
6
+ # @api public
7
+ # @example
8
+ # group_by('tasks.user_id', Task.all) { count('DISTINCT project_id') }
9
+ #
10
+ # @return [Multiple-Series]
11
+ def group_by(group_name, scope, label_block = nil, &eval_block)
12
+ group_values = [group_name] + scope.group_values
13
+ series = scope.except(:group).group(group_values).
14
+ instance_eval(&eval_block)
15
+
16
+ if label_block
17
+ update_key_from_block = lambda do |(key, data)|
18
+ if key.is_a?(Array)
19
+ group_key = key.first
20
+ sub_key = key[1..-1]
21
+ sub_key = sub_key.first if sub_key.length == 1
22
+ data = {sub_key => data}
23
+ else
24
+ group_key = key
25
+ end
26
+ (new_key, data) = label_block.call(group_key, data)
27
+ if key.is_a?(Array)
28
+ {[new_key, *Array.wrap(sub_key)] => data.values.first}
29
+ else
30
+ {new_key => data}
31
+ end
32
+ end
33
+
34
+ if series.length > 0
35
+ series_sum *series.map(&update_key_from_block)
36
+ else
37
+ {}
38
+ end
39
+ else
40
+ series
41
+ end
42
+ end
43
+
44
+
45
+ # @api public
46
+ # @example
47
+ # mean([0,1]) => {0.5}
48
+ #
49
+ # @return Value
50
+ def mean(array)
51
+ array.reduce(:+) / array.length
52
+ end
53
+
54
+ # @api public
55
+ # @return Value
56
+ def harmonic_mean(array)
57
+ array = array.reject(&:zero?)
58
+ return nil unless array.present?
59
+ array.size / (array.reduce(0) { |mean, value| mean + 1.0 / value })
60
+ end
61
+
62
+ # @api public
63
+ # @return Value
64
+ def geometric_mean(array)
65
+ array.reduce(:*) ** (1.0 / array.length)
66
+ rescue Math::DomainError
67
+ nil
68
+ end
69
+
70
+ # @api public
71
+ # @return Value
72
+ def grouped_median(series)
73
+ series.group_by(&:first).reduce({}) do |hash, (key, values)|
74
+ hash[key] = median(values.map(&:second))
75
+ hash
76
+ end
77
+ end
78
+
79
+ # @api public
80
+ # @return Value
81
+ def grouped_mean_and_median(series)
82
+ series.group_by(&:first).reduce({}) do |hash, (key, values)|
83
+ values = values.map(&:second).compact
84
+ next hash unless values.present?
85
+ hash[['median', key]] = median(values)
86
+ hash[['mean', key]] = mean(values)
87
+ hash[['geometric', key]] = geometric_mean(values)
88
+ hash[['harmonic', key]] = harmonic_mean(values)
89
+ hash
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,3 @@
1
+ class Hash
2
+ include Chartnado::Evaluator::Operators
3
+ end
@@ -0,0 +1,7 @@
1
+ require 'active_support/core_ext'
2
+
3
+ module Chartnado::Helper
4
+ def chartnado_eval(&block)
5
+ Chartnado.with_chartnado_dsl(&block)
6
+ end
7
+ end
@@ -0,0 +1,60 @@
1
+ require 'chartnado/renderer'
2
+ require 'chartkick'
3
+ require 'chartkick/remote/helper'
4
+
5
+ # These helpers can be included in your view using `helper Chartnado::Helpers::Chart`.
6
+ # They override the default chartkick chart rendering methods with ones that support chartnado DSL.
7
+ module Chartnado::Helpers
8
+ module Chart
9
+ include Chartkick::Helper
10
+ include Chartkick::Remote::Helper
11
+
12
+ def stacked_area_chart(*args, **options, &block)
13
+ Chartnado::Renderer.new(self, block) do |chartkick_options, json_options, data_block|
14
+ new_options = chartkick_options.reverse_merge(
15
+ stacked: true,
16
+ library: {
17
+ focusTarget: 'category',
18
+ series: {
19
+ 0 => {
20
+ lineWidth: 0,
21
+ pointSize: 0,
22
+ visibleInLegend: false
23
+ }
24
+ }
25
+ }
26
+ )
27
+ area_chart_without_chartnado(**new_options) do
28
+ data_block.call(json_options.reverse_merge(show_total: true, reverse_sort: true))
29
+ end
30
+ end.render(*args, **options)
31
+ end
32
+
33
+ def line_chart_with_chartnado(*args, **options, &block)
34
+ Chartnado::Renderer.new(self, block) do |chartkick_options, json_options, data_block|
35
+ new_options = chartkick_options.reverse_merge(
36
+ library: {
37
+ curveType: "none",
38
+ pointSize: 2,
39
+ focusTarget: 'category'
40
+ })
41
+ line_chart_without_chartnado(**new_options) do
42
+ data_block.call(**json_options)
43
+ end
44
+ end.render(*args, **options)
45
+ end
46
+
47
+ alias_method_chain :line_chart, :chartnado
48
+
49
+ %i{geo_chart pie_chart column_chart bar_chart area_chart}.each do |chart_type|
50
+ define_method(:"#{chart_type}_with_chartnado") do |*args, **options, &block|
51
+ Chartnado::Renderer.new(self, block) do |chartkick_options, json_options, data_block|
52
+ send(:"#{chart_type}_without_chartnado", **chartkick_options) do
53
+ data_block.call(**json_options)
54
+ end
55
+ end.render(*args, **options)
56
+ end
57
+ alias_method_chain chart_type, :chartnado
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,35 @@
1
+ module Chartnado::Helpers
2
+ module Series
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def define_series(name, &block)
7
+ define_method(name) do
8
+ Chartnado.with_chartnado_dsl do
9
+ self.instance_exec(&block)
10
+ end
11
+ end
12
+
13
+ memoized_name = :"@_memoized_#{name}"
14
+ define_method(:"#{name}_with_memoized") do |suffix: nil|
15
+ current_value = instance_variable_get(memoized_name)
16
+ if !current_value
17
+ series = send(:"#{name}_without_memoized")
18
+ instance_variable_set(memoized_name, series)
19
+ else
20
+ series = current_value
21
+ end
22
+ series = add_suffix_to_series_name(series, suffix) if suffix
23
+ series
24
+ end
25
+ alias_method_chain name, :memoized
26
+ end
27
+
28
+ def define_multiple_series(series)
29
+ series.each do |name, proc|
30
+ define_series(name, &proc)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,127 @@
1
+ class Chartnado::Renderer
2
+ include Chartnado::Series
3
+
4
+ attr_accessor :context, :data_block, :render_block
5
+
6
+ def initialize(context, data_block, &render_block)
7
+ @context = context
8
+ @data_block = data_block
9
+ @render_block = render_block
10
+ end
11
+
12
+ delegate :controller, to: :context
13
+
14
+ def render(*args, **options)
15
+ json_options = {}
16
+ chartkick_options = options.dup
17
+
18
+ if args.length > 1
19
+ if args.last.is_a?(Hash)
20
+ json_options = chartkick_options
21
+ chartkick_options = args[1].dup
22
+ end
23
+ args.select { |arg| arg.is_a?(Symbol) }.each do |key|
24
+ json_options[key] = true unless json_options.has_key?(key)
25
+ end
26
+ end
27
+
28
+ if json_options[:percentage]
29
+ chartkick_options.reverse_merge!(max: 100.0)
30
+ end
31
+
32
+ options = controller.chartnado_options if controller.respond_to?(:chartnado_options)
33
+ options ||= {}
34
+
35
+ chart_json_proc = -> (*args) {
36
+ chart_json(Chartnado.with_chartnado_dsl(&data_block), *args)
37
+ }
38
+ renderer = -> {
39
+ context.instance_exec(chartkick_options, json_options, chart_json_proc, &render_block)
40
+ }
41
+
42
+ if options[:wrapper_proc]
43
+ context.instance_exec(*args, renderer, **options, &options[:wrapper_proc])
44
+ else
45
+ renderer.call
46
+ end
47
+ end
48
+
49
+ def chart_json(series, show_total: false, reverse_sort: false, percentage: false)
50
+ series = series_product(100.0, series) if percentage
51
+ if series.is_a?(Hash)
52
+ if (key = series.keys.first) and key.is_a?(Array) and key.size == 2
53
+ totals = Hash.new(0.0)
54
+ new_series = series.group_by{|k, v| k[0] }.sort_by { |k| k.to_s }
55
+ new_series = new_series.reverse if reverse_sort
56
+
57
+ new_series = new_series.map do |name, data|
58
+ {
59
+ name: name,
60
+ data: data.map do |k, v|
61
+ totals[k[1]] += v if show_total
62
+ [k[1], v]
63
+ end
64
+ }
65
+ end
66
+
67
+ if show_total
68
+ [{name: 'Total',
69
+ data: totals.map {|k,v| [k, 0] },
70
+ tooltip: totals.map {|k,v| [k, v] }
71
+ }] + new_series
72
+ else
73
+ new_series
74
+ end
75
+ else
76
+ new_series = series.sort_by { |key| key.to_s }
77
+ new_series = new_series.reverse if reverse_sort
78
+
79
+ if show_total
80
+ new_series << ['Total', new_series.map(&:second).reduce(0, :+)]
81
+ else
82
+ new_series
83
+ end
84
+ end
85
+ elsif series.is_a?(Array) && series.first.is_a?(Array)
86
+ if series.first.second.respond_to?(:map)
87
+ totals = Hash.new(0.0)
88
+ new_series = series.sort_by { |item| item.first.to_s }
89
+ new_series = new_series.reverse if reverse_sort
90
+
91
+ new_series = new_series.map do |name, data|
92
+ {
93
+ name: name,
94
+ data: data.map do |k, v|
95
+ totals[k] += v if show_total
96
+ [k, v]
97
+ end
98
+ }
99
+ end
100
+
101
+ if show_total
102
+ [{name: 'Total',
103
+ data: totals.map {|k,v| [k, 0] },
104
+ tooltip: totals.map {|k,v| [k, v] }
105
+ }] + new_series
106
+ else
107
+ new_series
108
+ end
109
+ else
110
+ new_series = series.sort_by { |item| item.first.to_s }
111
+ new_series = new_series.reverse if reverse_sort
112
+
113
+ if show_total
114
+ new_series << ['Total', new_series.map(&:second).reduce(0, :+)]
115
+ else
116
+ new_series
117
+ end
118
+ end
119
+ else
120
+ if series.respond_to?(:map)
121
+ series
122
+ else
123
+ [['Total', series]]
124
+ end
125
+ end
126
+ end
127
+ end