chartnado 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,186 @@
1
+ require 'active_support/core_ext'
2
+
3
+ module Chartnado
4
+ module Series
5
+ # @api public
6
+ # @example
7
+ # series_product(2.0, {0 => 1}) => {0 => 2.0}
8
+ # series_product({0 => 1}, 2.0) => {0 => 2.0}
9
+ #
10
+ # @return [Series/Multiple-Series]
11
+ def series_product(val, series, precision: 2)
12
+ if dimensions(val) > dimensions(series)
13
+ return series_product(series, val)
14
+ end
15
+
16
+ return with_precision(precision, val.to_f * series.to_f) unless series.respond_to?(:length)
17
+ return series unless series.length > 0
18
+
19
+ if is_an_array_of_named_series?(series) || series.is_a?(Array) && series.first.is_a?(Array)
20
+ series.map { |(name, data)| [name, series_product(val, data)] }
21
+ elsif series.is_a?(Hash)
22
+ series.to_a.reduce({}) do |hash, (key, value)|
23
+ if val.is_a?(Hash)
24
+ if key.is_a?(Array)
25
+ scalar = val[key.second]
26
+ else
27
+ scalar = val[key]
28
+ end
29
+ else
30
+ scalar = val
31
+ end
32
+ scalar ||= 0
33
+ hash[key] = scalar * value
34
+ hash
35
+ end
36
+ else
37
+ series.map do |value|
38
+ val * value
39
+ end
40
+ end
41
+ end
42
+
43
+ # @api public
44
+ # @example
45
+ # series_ratio({0 => 1}, 2.0) => {0 => 0.5}
46
+ #
47
+ # @return [Series/Multiple-Series]
48
+ def series_ratio(top_series, bottom_series, multiplier: 1.0, precision: 2)
49
+ if bottom_series.is_a?(Numeric)
50
+ return series_product(1.0 * multiplier / bottom_series, top_series, precision: precision)
51
+ end
52
+ if has_multiple_series?(top_series) && !has_multiple_series?(bottom_series)
53
+ top_series_by_name = data_by_name(top_series)
54
+ if is_an_array_of_named_series?(top_series)
55
+ top_series_by_name.map do |name, top_values|
56
+ [
57
+ name,
58
+ series_ratio(top_values, bottom_series, multiplier: multiplier, precision: precision)
59
+ ]
60
+ end
61
+ else
62
+ bottom_series.reduce({}) do |hash, (key, value)|
63
+ top_series_by_name.keys.each do |name|
64
+ top_key = [name, *key]
65
+ top_value = top_series_by_name[name][top_key]
66
+ if top_value
67
+ hash[top_key] = series_ratio(top_value, value, multiplier: multiplier, precision: precision)
68
+ end
69
+ end
70
+ hash
71
+ end
72
+ end
73
+ elsif is_an_array_of_named_series?(bottom_series)
74
+ top_series_by_name = data_by_name(top_series)
75
+ bottom_series.map do |(name, data)|
76
+ [
77
+ name,
78
+ series_ratio(top_series_by_name[name], data, multiplier: multiplier, precision: precision)
79
+ ]
80
+ end
81
+ elsif bottom_series.respond_to?(:reduce)
82
+ bottom_series.reduce({}) do |hash, (key, value)|
83
+ hash[key] = series_ratio(top_series[key] || 0, value, multiplier: multiplier, precision: precision)
84
+ hash
85
+ end
86
+ else
87
+ with_precision(precision, top_series.to_f * multiplier.to_f / bottom_series.to_f)
88
+ end
89
+ end
90
+
91
+ # @api public
92
+ # @example
93
+ # series_sum({0 => 1}, 2.0) => {0 => 3.0}
94
+ # series_sum({0 => 1}, {0 => 1}) => {0 => 2}
95
+ # series_sum({0 => 1}, 2.0, 3.0) => {0 => 6.0}
96
+ # series_sum(1, 2) => 3
97
+ # series_sum() => []
98
+ #
99
+ # @return [Series/Multiple-Series/Scalar]
100
+ def series_sum(*series, scalar_sum: 0.0)
101
+ return [] unless series.length > 0
102
+
103
+ (series, scalars) = series.partition { |s| s.respond_to?(:map) }
104
+ scalar_sum += scalars.reduce(:+) || 0.0
105
+
106
+ if series.first.is_a?(Hash)
107
+ keys = series.map(&:keys).flatten(1).uniq
108
+ keys.reduce({}) do |hash, key|
109
+ hash[key] = (series.map { |s| s[key] }.compact.reduce(:+) || 0) + scalar_sum
110
+ hash
111
+ end
112
+ elsif is_an_array_of_named_series?(series.first)
113
+ series.flatten(1).group_by(&:first).map do |name, values|
114
+ data = values.map(&:second).reduce(Hash.new(scalar_sum)) do |hash, values|
115
+ values.each do |key, value|
116
+ hash[key] += value
117
+ end
118
+ hash
119
+ end
120
+ [
121
+ name, data
122
+ ]
123
+ end
124
+ elsif series.first.is_a?(Array)
125
+ series.map { |s| s.reduce(:+) + scalar_sum }
126
+ else
127
+ scalar_sum
128
+ end
129
+ end
130
+
131
+ # @api public
132
+ # @example
133
+ # median([0,1]) => {0.5}
134
+ # median([0,1,1,2,2]) => {1}
135
+ #
136
+ # @return Value
137
+ def median(array)
138
+ sorted = array.sort
139
+ len = sorted.length
140
+ (sorted[(len - 1) / 2] + sorted[len / 2]) / 2.0
141
+ end
142
+
143
+ private
144
+
145
+ def data_by_name(series)
146
+ if is_an_array_of_named_series?(series)
147
+ series.reduce({}) do |hash, value|
148
+ hash[value.first] = value.second
149
+ hash
150
+ end
151
+ else
152
+ series.reduce({}) do |hash, (key, value)|
153
+ new_key = Array.wrap(key.first).first
154
+ hash[new_key] = {key => value }
155
+ hash
156
+ end
157
+ end
158
+ end
159
+
160
+ def series_names(series)
161
+ series.map { |key| key.first }.uniq
162
+ end
163
+
164
+ def has_multiple_series?(series)
165
+ is_an_array_of_named_series?(series) || series.is_a?(Hash) && series.first && series.first[0].is_a?(Array) && series.first[0].length > 1
166
+ end
167
+
168
+ def is_an_array_of_named_series?(series)
169
+ series.is_a?(Array) && series.first.second.is_a?(Hash)
170
+ end
171
+
172
+ def dimensions(series)
173
+ return 1 unless series.respond_to?(:length)
174
+ if series.first && series.first.is_a?(Array)
175
+ 3
176
+ else
177
+ 2
178
+ end
179
+ end
180
+
181
+ def with_precision(precision, value)
182
+ value = value.round(precision) if precision
183
+ value
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,3 @@
1
+ module Chartnado
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+ require 'rails_helper'
3
+
4
+ describe "Controller Methods", type: :controller do
5
+ render_views
6
+
7
+ routes do
8
+ ActionDispatch::Routing::RouteSet.new.tap do |routes|
9
+ routes.draw { get "show" => "anonymous#show" }
10
+ end
11
+ end
12
+
13
+ controller do
14
+ include Chartnado
15
+
16
+ chartkick_remote remote: false
17
+
18
+ define_method :_routes do
19
+ ActionDispatch::Routing::RouteSet.new.tap do |routes|
20
+ routes.draw { get "show" => "anonymous#show" }
21
+ end
22
+ end
23
+
24
+ def show
25
+ end
26
+ end
27
+
28
+ describe ".chartnado_wrapper" do
29
+ describe "when the wrapper is referenced by symbol" do
30
+ before do
31
+ controller.singleton_class.class_eval do
32
+ chartnado_wrapper :wrap_chart
33
+
34
+ def show
35
+ render inline: "<% area_chart { {0 => 1} / 2.0 } %>"
36
+ end
37
+ end
38
+ end
39
+
40
+ it "calls the wrapper in the context of the helpers" do
41
+ routes.draw { get "show" => "anonymous#show" }
42
+ expect(controller).to receive(:wrap_chart)
43
+ get :show
44
+ end
45
+ end
46
+
47
+ describe "when the wrapper is desribed by a block" do
48
+ before do
49
+ controller.singleton_class.class_eval do
50
+ chartnado_wrapper do |*args, **options, &block|
51
+ throw :wrapper_was_called
52
+ end
53
+
54
+ def show
55
+ render inline: "<% area_chart { {0 => 1} / 2.0 } %>"
56
+ end
57
+ end
58
+ end
59
+
60
+ it "calls the wrapper in the context of the helpers" do
61
+ routes.draw { get "show" => "anonymous#show" }
62
+ expect {
63
+ get :show
64
+ }.to throw_symbol :wrapper_was_called
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chartnado do
4
+ it "provides a dsl for vector operations" do
5
+ expect(Chartnado.with_chartnado_dsl { {a: 2} / 2 }).to eq({a: 1})
6
+ end
7
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+ require 'rails_helper'
3
+
4
+ describe Chartnado::Helpers::Chart, type: :helper do
5
+ def works?(&block)
6
+ expect {
7
+ block.call
8
+ }.not_to raise_exception
9
+ end
10
+
11
+ describe "#area_chart" do
12
+ it "supports the dsl" do
13
+ works? { helper.area_chart { {1 => 2} / 2.0 } }
14
+ end
15
+
16
+ describe "with a custom renderer" do
17
+ it "calls the custom renderer" do
18
+ wrapper_proc = proc { throw :wrapper_was_called }
19
+ expect {
20
+ expect(controller).to receive(:chartnado_options).and_return({wrapper_proc: wrapper_proc})
21
+ helper.area_chart { {1 => 2} / 2.0 }
22
+ }.to throw_symbol :wrapper_was_called
23
+ end
24
+ end
25
+ end
26
+
27
+ describe "#stacked_area_chart" do
28
+ it "supports the dsl" do
29
+ works? { helper.stacked_area_chart { {1 => 2} / 2.0 } }
30
+ end
31
+ describe "percentage option" do
32
+ end
33
+ end
34
+
35
+ describe "#pie_chart" do
36
+ it "supports the dsl" do
37
+ works? { helper.pie_chart { {1 => 2} / 2.0 } }
38
+ end
39
+ end
40
+
41
+ describe "#geo_chart" do
42
+ it "supports the dsl" do
43
+ works? { helper.geo_chart { {1 => 2} / 2.0 } }
44
+ end
45
+ end
46
+
47
+ describe "#line_chart" do
48
+ it "supports the dsl" do
49
+ works? { helper.line_chart { {1 => 2} / 2.0 } }
50
+ end
51
+ xit "includes total"
52
+ describe "percentage option" do
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'rails_helper'
3
+
4
+ describe Chartnado::Helpers::Series, type: :helper do
5
+ let(:test_class) {
6
+ Class.new do
7
+ include Chartnado::Helpers::Series
8
+ end
9
+ }
10
+
11
+ describe ".define_series" do
12
+ it "lets the user define a series using the chartnado dsl" do
13
+ test_class.class_eval do
14
+ define_series :my_series do
15
+ {0 => 1} / 2
16
+ end
17
+ end
18
+ expect(test_class.new.my_series).to eq({0 => 0.5})
19
+ end
20
+ end
21
+ describe ".define_multiple_series" do
22
+ it "lets the user define multiple series using the chartnado dsl" do
23
+ test_class.class_eval do
24
+ define_multiple_series(
25
+ my_first_series: -> { {0 => 1} / 2 },
26
+ my_second_series: -> { {1 => 1} / 2 }
27
+ )
28
+ end
29
+ expect(test_class.new.my_first_series).to eq({0 => 0.5})
30
+ expect(test_class.new.my_second_series).to eq({1 => 0.5})
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ require 'action_controller'
2
+ require 'action_view'
3
+
4
+ require 'rspec/rails'
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chartnado::Renderer do
4
+ describe "#chart_json" do
5
+ def chart_json(*series, **options)
6
+ Chartnado::Renderer.new(nil, nil).chart_json(*series, **options)
7
+ end
8
+
9
+ describe "for data formatted as a hash" do
10
+ it "can generate chartkick compatible series" do
11
+ expect(chart_json({[:a, 1] => 10, [:b, 1] => 20})).
12
+ to eq [{name: :a, data: [[1, 10]]}, {name: :b, data: [[1,20]]}]
13
+ end
14
+ it "can add totals" do
15
+ expect(chart_json({[:a, 1] => 10, [:b, 1] => 20}, show_total: true)).
16
+ to eq [{name: 'Total', data: [[1, 0]], tooltip: [[1, 30.0]]},
17
+ {name: :a, data: [[1, 10]]},
18
+ {name: :b, data: [[1, 20]]}]
19
+ end
20
+ describe "with multiple scalar series" do
21
+ it "can handle scalars" do
22
+ expect(chart_json({:a => 10, :b => 20})).
23
+ to eq([[:a, 10], [:b, 20]])
24
+ end
25
+ it "can add totals" do
26
+ expect(chart_json({:a => 10, :b => 20}, show_total: true)).
27
+ to eq([[:a, 10], [:b, 20], ['Total', 30]])
28
+ end
29
+ end
30
+ end
31
+ describe "for data formatted as an array" do
32
+ it "can generate chartkick compatible series" do
33
+ expect(chart_json([[:a, {1 => 10}], [:b, {1 => 20}]])).
34
+ to eq [{name: :a, data: [[1, 10]]}, {name: :b, data: [[1,20]]}]
35
+ end
36
+ it "can add totals" do
37
+ expect(chart_json([[:a, {1 => 10}], [:b, {1 => 20}]], show_total: true)).
38
+ to eq [{name: 'Total', data: [[1, 0]], tooltip: [[1, 30.0]]},
39
+ {name: :a, data: [[1, 10]]},
40
+ {name: :b, data: [[1, 20]]}]
41
+ end
42
+ end
43
+ describe "for data that is just a scalar" do
44
+ it "shows the scalar as the total" do
45
+ expect(chart_json(10)).
46
+ to eq [['Total', 10]]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,138 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chartnado::Series do
4
+ before do
5
+ class << self
6
+ include Chartnado::Series
7
+ end
8
+ end
9
+
10
+ describe "#series_product" do
11
+ describe "multiplying a hash by a scalar" do
12
+ it "returns the product of a scalar and a hash" do
13
+ expect(series_product(2, {0 => 3})).to eq ({0 => 6})
14
+ end
15
+ end
16
+ describe "multiplying an array by a scalar" do
17
+ it "returns the product of a scalar and a hash" do
18
+ expect(series_product(2, [3, 7])).to eq [6, 14]
19
+ end
20
+ end
21
+ describe "multiplying an scalar by a scalar" do
22
+ it "returns the product of a scalar and a hash" do
23
+ expect(series_product(2, 3)).to eq 6
24
+ end
25
+ end
26
+ describe "multiplying an hash of named series by a scalar" do
27
+ it "returns the product of a scalar and each named_series" do
28
+ expect(
29
+ series_product(
30
+ 2,
31
+ {[:series_a, 0] => 3,
32
+ [:series_b, 1] => 4}
33
+ )).to eq ({[:series_a, 0] => 6, [:series_b, 1] => 8})
34
+ end
35
+ end
36
+ describe "multiplying an array of named series by a scalar" do
37
+ it "returns the product of a scalar and each named_series" do
38
+ expect(
39
+ series_product(
40
+ 2,
41
+ [[:series_a, {0 => 3}],
42
+ [:series_b, {1 => 4}]]
43
+ )).to eq [[:series_a, {0 => 6}], [:series_b, {1 => 8}]]
44
+ end
45
+ end
46
+ end
47
+
48
+ describe "#series_sum" do
49
+ describe "adding two scalars" do
50
+ it "returns the sum of the scalars" do
51
+ expect(series_sum(2,3)).to eq 5
52
+ end
53
+ end
54
+ describe "adding a scalar to an array" do
55
+ it "returns each item of the array with a scalar added" do
56
+ expect(series_sum(2,[3])).to eq [5]
57
+ end
58
+ end
59
+ describe "adding a scalar to a hash" do
60
+ it "returns each item of the array with a scalar added" do
61
+ expect(series_sum(2,{0 => 3})).to eq ({0 => 5})
62
+ end
63
+ end
64
+ describe "adding a scalar to an array of named series" do
65
+ it "returns each item of the array with a scalar added" do
66
+ expect(series_sum(2,[[:a, {0 => 3}]])).to eq ([[:a, {0 => 5}]])
67
+ end
68
+ end
69
+ describe "adding two hashes" do
70
+ it "returns each item of the array with a scalar added" do
71
+ expect(series_sum({0 => 1},{0 => 2})).to eq ({0 => 3})
72
+ end
73
+ end
74
+ describe "adding two hashes and a scalar" do
75
+ it "returns each item of the array with a scalar added" do
76
+ expect(series_sum({0 => 1},{0 => 2}, 5)).to eq ({0 => 8})
77
+ end
78
+ end
79
+ end
80
+
81
+ describe "#series_ratio" do
82
+ describe "ratio of two scalars" do
83
+ it "returns the ratio" do
84
+ expect(series_ratio(1, 2)).to eq 0.5
85
+ end
86
+ end
87
+ describe "ratio of two hashes" do
88
+ it "returns the ratio" do
89
+ expect(series_ratio({0 => 1}, {0 => 2})).to eq ({0 => 0.5})
90
+ end
91
+ end
92
+ describe "ratio of a named series to another named series" do
93
+ it "returns the ratio" do
94
+ expect(series_ratio({[:series_a, 0] => 1},
95
+ {[:series_a, 0] => 2})).to eq ({[:series_a, 0] => 0.5})
96
+ end
97
+ end
98
+ describe "ratio of an array of named series to another array of named series" do
99
+ it "returns the ratio" do
100
+ expect(series_ratio([[:series_a, {0 => 1}]],
101
+ [[:series_a, {0 => 2}]])).to eq [[:series_a, {0 => 0.5}]]
102
+ end
103
+ end
104
+ describe "ratio of a named series to a non-named series" do
105
+ it "returns the ratio" do
106
+ expect(series_ratio({[:series_a, 0] => 1},
107
+ {0 => 2})).to eq ({[:series_a, 0] => 0.5})
108
+ end
109
+ end
110
+ describe "ratio of an array of named series to a non-named series" do
111
+ it "returns the ratio" do
112
+ expect(series_ratio([[:series_a, {0 => 1}]],
113
+ {0 => 2})).to eq [[:series_a, {0 => 0.5}]]
114
+ end
115
+ end
116
+ describe "ratio of a series to a scalar" do
117
+ xit "returns the ratio" do
118
+ expect(series_ratio({0 => 1}, 2)).to eq ({0 => 0.5})
119
+ end
120
+ end
121
+
122
+ describe "including a multiplier" do
123
+ describe "for ratio of two scalars" do
124
+ it "returns the ratio times the multiplier" do
125
+ expect(series_ratio(1, 2, multiplier: 100)).to eq 50
126
+ end
127
+ end
128
+ end
129
+
130
+ describe "specifying the precision" do
131
+ describe "for ratio of two scalars" do
132
+ it "returns the ratio rounded to the specified precision" do
133
+ expect(series_ratio(1, 3, precision: 1)).to eq 0.3
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end