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,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