chartnado 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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