mountain-goat 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/README.md +5 -4
- data/generators/mg/mg_generator.rb +2 -2
- data/generators/mg/templates/update_mountain_goat_tables_v2.rb +73 -0
- data/lib/mountain-goat/controllers/mg/choices_controller.rb +81 -0
- data/lib/mountain-goat/controllers/mg/goals_controller.rb +136 -0
- data/lib/mountain-goat/controllers/mg/records_controller.rb +46 -0
- data/lib/mountain-goat/controllers/mg/tests_controller.rb +139 -0
- data/lib/mountain-goat/mg_core.rb +403 -0
- data/lib/mountain-goat/models/mg/choice.rb +50 -0
- data/lib/mountain-goat/models/mg/gi_meta.rb +10 -0
- data/lib/mountain-goat/models/mg/goal.rb +183 -0
- data/lib/mountain-goat/models/mg/goal_meta_type.rb +20 -0
- data/lib/mountain-goat/models/mg/gs_meta.rb +10 -0
- data/lib/mountain-goat/models/mg/record.rb +59 -0
- data/lib/mountain-goat/models/mg/test.rb +31 -0
- data/lib/mountain-goat/public/g-funnel.js +168 -0
- data/lib/mountain-goat/switch_choice.rb +32 -0
- data/lib/mountain-goat/version.rb +1 -1
- data/lib/mountain-goat/views/mountain_goat/layouts/.tmp_mountain_goat.html.erb.50680~ +76 -0
- data/lib/mountain-goat/views/mountain_goat/mg/choices/_choice_form.html.erb +37 -0
- data/lib/mountain-goat/views/mountain_goat/mg/choices/edit.html.erb +13 -0
- data/lib/mountain-goat/views/mountain_goat/mg/choices/index.html.erb +34 -0
- data/lib/mountain-goat/views/mountain_goat/mg/choices/new.html.erb +15 -0
- data/lib/mountain-goat/views/mountain_goat/mg/choices/show.html.erb +30 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/.tmp__goal_form.html.erb.5027~ +0 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/.tmp__goal_meta_type_form.html.erb.39992~ +0 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/.tmp_edit.html.erb.55874~ +0 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/.tmp_index.html.erb.97274~ +36 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/_goal_form.html.erb +28 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/_goal_meta_type_form.html.erb +34 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/edit.html.erb +12 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/index.html.erb +37 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/new.html.erb +12 -0
- data/lib/mountain-goat/views/mountain_goat/mg/goals/show.html.erb +32 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/_record.html.erb +16 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/_records.html.erb +5 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/_records_form.html.erb +21 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/edit.html.erb +13 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/index.html.erb +17 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/new.html.erb +13 -0
- data/lib/mountain-goat/views/mountain_goat/mg/records/show.html.erb +14 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp__chart.html.erb.13419~ +18 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp__funnel.html.erb.60493~ +18 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp__report_item_form.html.erb.87420~ +10 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp__report_item_pivot_form.html.erb.77056~ +14 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp_edit.html.erb.31048~ +19 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/.tmp_new.html.erb.36371~ +17 -0
- data/lib/mountain-goat/views/mountain_goat/mg/report_items/_funnel.html.erb +13 -0
- data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp__report_form.html.erb.76535~ +21 -0
- data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp__report_report_items.html.erb.26030~ +5 -0
- data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp_edit.html.erb.78064~ +36 -0
- data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp_index.html.erb.74591~ +23 -0
- data/lib/mountain-goat/views/mountain_goat/mg/reports/.tmp_show.html.erb.58427~ +21 -0
- data/lib/mountain-goat/views/mountain_goat/mg/tests/_test_form.html.erb +25 -0
- data/lib/mountain-goat/views/mountain_goat/mg/tests/edit.html.erb +12 -0
- data/lib/mountain-goat/views/mountain_goat/mg/tests/index.html.erb +48 -0
- data/lib/mountain-goat/views/mountain_goat/mg/tests/new.html.erb +12 -0
- data/lib/mountain-goat/views/mountain_goat/mg/tests/show.html.erb +63 -0
- metadata +69 -6
- data/lib/mountain-goat/metric_tracking.rb +0 -401
- data/lib/mountain-goat/switch_variant.rb +0 -32
@@ -0,0 +1,10 @@
|
|
1
|
+
class Mg::GiMeta < ActiveRecord::Base
|
2
|
+
set_table_name :mg_gi_metas
|
3
|
+
|
4
|
+
belongs_to :mg_goal_meta_type, :class_name => "Mg::GoalMetaType"
|
5
|
+
belongs_to :mg_record, :class_name => "Mg::Record"
|
6
|
+
|
7
|
+
validates_presence_of :mg_goal_meta_type_id
|
8
|
+
validates_presence_of :mg_record_id
|
9
|
+
|
10
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# Mg::Goal represents a target for user interaction (E.g. Sign-up)
|
2
|
+
#
|
3
|
+
# Attributes
|
4
|
+
# goal_type:: A symbol uniquely identifying this goal type (for code interactions)
|
5
|
+
# name:: A name for this goal
|
6
|
+
# deleted_at:: Is this goal deleted? (MG Console)
|
7
|
+
# is_hidden:: Is this goal hidden? (MG Console)
|
8
|
+
class Mg::Goal < ActiveRecord::Base
|
9
|
+
set_table_name :mg_goals
|
10
|
+
|
11
|
+
# ActiveRecord Associations
|
12
|
+
has_many :mg_records, :class_name => "Mg::Record", :foreign_key => "mg_goal_id"
|
13
|
+
has_many :mg_goal_meta_types, :class_name => "Mg::GoalMetaType", :foreign_key => "mg_goal_id"
|
14
|
+
has_many :gi_metas, :through => :mg_goal_meta_types, :class_name => "Mg::GiMeta", :foreign_key => "mg_goal_id"
|
15
|
+
has_many :gs_metas, :through => :mg_goal_meta_types, :class_name => "Mg::GsMeta", :foreign_key => "mg_goal_id"
|
16
|
+
has_many :report_items, :as => :reportable, :class_name => "Mg::ReportItem"
|
17
|
+
|
18
|
+
# Secondary Associations
|
19
|
+
accepts_nested_attributes_for :mg_goal_meta_types, :reject_if => lambda { |a| a[:name].blank? || a[:var].blank? || a[:meta_type].blank? }, :allow_destroy => true
|
20
|
+
|
21
|
+
# Validations
|
22
|
+
validates_presence_of :name
|
23
|
+
validates_format_of :goal_type, :with => /[a-z0-9_]{3,50}/i, :message => "must be between 3 and 30 characters, alphanumeric with underscores"
|
24
|
+
validates_uniqueness_of :goal_type
|
25
|
+
|
26
|
+
# Member Functions
|
27
|
+
|
28
|
+
# Helper function to retrieve a goal by symbol
|
29
|
+
def self.by_type(s)
|
30
|
+
Mg::Goal.find( :first, :conditions => { :mg_goal_type => s.to_s } )
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get all records for given meta (e.g. for "Referer": { "Youtube" => Record1, } ...)
|
34
|
+
def records_for_meta(var)
|
35
|
+
gmt = self.mg_goal_meta_types.find_by_var( var.to_s )
|
36
|
+
return {} if gmt.nil?
|
37
|
+
gmt.meta.map { |m| { m.data => m.record } }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get all records for a given meta value (e.g. for "Referer", "Facebook": [ Record1, Record2, Record3 ] )
|
41
|
+
def records_for_meta_val(var, data)
|
42
|
+
gmt = self.mg_goal_meta_types.find_by_var( var.to_s )
|
43
|
+
return [] if gmt.nil?
|
44
|
+
gmt.meta.find(:all, :conditions => { :data => data } ).map { |m| m.record }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get all records pivoted by given meta (e.g. "Youtube" => [ Date, Date, Date ], "Facebook" => [ Date, Date, Date ])
|
48
|
+
def records_pivot(pivot)
|
49
|
+
res = {}
|
50
|
+
gmt_pivot = self.mg_goal_meta_types.find_by_var( pivot.to_s )
|
51
|
+
return {} if gmt_pivot.nil?
|
52
|
+
gmt_pivot.meta.map { |c| { :created_at => c.created_at, :pivot => c.data } }.each do |c|
|
53
|
+
if !res.include?(c[:pivot])
|
54
|
+
res[c[:pivot]] = []
|
55
|
+
end
|
56
|
+
|
57
|
+
res[c[:pivot]].push c[:created_at]
|
58
|
+
end
|
59
|
+
|
60
|
+
res.each { |k,v| v.sort! }
|
61
|
+
res
|
62
|
+
end
|
63
|
+
|
64
|
+
def records_for_meta_val_pivot(var, data, pivot)
|
65
|
+
res = {}
|
66
|
+
gmt = self.mg_goal_meta_types.find_by_var( var.to_s )
|
67
|
+
gmt_pivot = self.mg_goal_meta_types.find_by_var( pivot.to_s )
|
68
|
+
return {} if gmt.nil? || gmt_pivot.nil?
|
69
|
+
gmt.meta.find(:all, :select => "`#{gmt.meta.table_name}`.created_at, gm.data AS pivot", :conditions => { :data => data }, :joins => "LEFT JOIN `#{gmt_pivot.meta.table_name}` gm ON gm.mg_goal_meta_type_id = #{gmt_pivot.id} AND gm.mg_record_id = `#{gmt.meta.table_name}`.mg_record_id").each do |c|
|
70
|
+
if !res.include?(c.pivot)
|
71
|
+
res[c.pivot] = []
|
72
|
+
end
|
73
|
+
|
74
|
+
res[c.pivot].push c.created_at
|
75
|
+
end
|
76
|
+
|
77
|
+
res.each { |k,v| v.sort! }
|
78
|
+
res
|
79
|
+
end
|
80
|
+
|
81
|
+
# Pivot by given meta data by value (TODO: Document better)
|
82
|
+
def records_for_meta_val_pivot_item(var, data, pivot, item)
|
83
|
+
res = {}
|
84
|
+
gmt = self.mg_goal_meta_types.find_by_var( var.to_s )
|
85
|
+
gmt_pivot = self.mg_goal_meta_types.find_by_var( pivot.to_s )
|
86
|
+
gmt_item = self.mg_goal_meta_types.find_by_var( item.to_s )
|
87
|
+
return {} if gmt.nil? || gmt_pivot.nil? || gmt_item.nil?
|
88
|
+
gmt.meta.find(:all, :select => "`#{gmt.meta.table_name}`.created_at, gm.data AS pivot, gi.data as item", :conditions => { :data => data }, :joins => "LEFT JOIN `#{gmt_pivot.meta.table_name}` gm ON gm.mg_goal_meta_type_id = #{gmt_pivot.id} AND gm.mg_record_id = `#{gmt.meta.table_name}`.mg_record_id LEFT JOIN `#{gmt_item.meta.table_name}` gi ON gi.mg_goal_meta_type_id = #{gmt_item.id} AND gi.mg_record_id = `#{gmt.meta.table_name}`.mg_record_id").each do |c|
|
89
|
+
if !res.include?(c.pivot)
|
90
|
+
res[c.pivot] = []
|
91
|
+
end
|
92
|
+
|
93
|
+
if gmt_item.meta_type == 'gi_meta'
|
94
|
+
c.item.to_i.times { res[c.pivot].push c.created_at }
|
95
|
+
else
|
96
|
+
res[c.pivot].push c.created_at if c.item == 'yes' #what else?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
res.each { |k,v| v.sort! }
|
101
|
+
res.delete_if { |k,v| v.count == 0 }
|
102
|
+
res
|
103
|
+
end
|
104
|
+
|
105
|
+
# Get recent records based on start date
|
106
|
+
def recent_records(time_frame)
|
107
|
+
self.mg_records.find( :all, :conditions => [ "CREATED_AT > ?", time_frame.ago ] )
|
108
|
+
end
|
109
|
+
|
110
|
+
# Tally we have given a reward
|
111
|
+
def tally_reward_given( reward )
|
112
|
+
self.transaction do
|
113
|
+
self.update_attribute(:rewards_total, 0) if self.rewards_total.nil? #we should merge this with the next line, but whatever
|
114
|
+
Mg::Goal.update_counters(self.id, :rewards_given => 1)
|
115
|
+
Mg::Goal.update_counters(self.id, :rewards_total => reward)
|
116
|
+
end
|
117
|
+
|
118
|
+
return self.reload
|
119
|
+
end
|
120
|
+
|
121
|
+
#For Reportable
|
122
|
+
|
123
|
+
# Title in report charts
|
124
|
+
def reportable_title(pivot)
|
125
|
+
if pivot.nil?
|
126
|
+
return self.name
|
127
|
+
elsif pivot.instance_of?(Mg::GoalMetaType)
|
128
|
+
return "#{self.name} by #{pivot.name}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
def reportable_chart_items(pivot)
|
134
|
+
#let's look for pageviews by day by source
|
135
|
+
#rallies_for_meta_val( :clique_id, @clique.id )
|
136
|
+
#logger.debug "sources: #{sources.inspect}"
|
137
|
+
#We now have a map of { source => [ date1, date2, ... ], ... }
|
138
|
+
|
139
|
+
#Now, we just need to insert missing data (and group)
|
140
|
+
#Let's transpose that into { source => [ :x => day0, y => count ] }
|
141
|
+
if pivot.nil?
|
142
|
+
return Analytics.pivot_by_date( { :"Records" => self.mg_records.map { |r| r.created_at } }, self.created_at )
|
143
|
+
elsif pivot.instance_of?(Mg::GoalMetaType)
|
144
|
+
sources = self.records_pivot( pivot.var )
|
145
|
+
logger.warn "sources: #{sources}"
|
146
|
+
return Analytics.pivot_by_date(sources, self.created_at)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
#
|
151
|
+
def reportable_gerbil_chart(pivot)
|
152
|
+
#chart = GerbilCharts::Charts::LineChart.new( :width => 350, :height => 200, :style => 'brushmetal.css', :circle_data_points => true )
|
153
|
+
|
154
|
+
if pivot.nil?
|
155
|
+
data = Analytics.pivot_by_date( { :"Records" => self.mg_records.map { |r| r.created_at } }, self.created_at )
|
156
|
+
elsif pivot.instance_of?(Mg::GoalMetaType)
|
157
|
+
sources = self.records_pivot( pivot.var )
|
158
|
+
data = Analytics.pivot_by_date(sources, self.created_at)
|
159
|
+
end
|
160
|
+
#logger.warn "data: #{data.inspect}"
|
161
|
+
#logger.warn "ts: #{data.map { |line| line[1].map { |d| d[:x] }.to_a }[0].inspect}"
|
162
|
+
#logger.warn "val: #{data.map { |line| [ line[0] ] + line[1].map { |d| d[:y] }.to_a }[0].inspect}"
|
163
|
+
#chart.modelgroup = GerbilCharts::Models::SimpleTimeSeriesModelGroup.new(
|
164
|
+
# :title => "Rallies",
|
165
|
+
# :timeseries => data.map { |line| line[1].map { |d| d[:x].to_time }.to_a }[0],
|
166
|
+
# :models => data.map { |line| [ line[0].to_s ] + line[1].map { |d| d[:y] }.to_a }
|
167
|
+
#)
|
168
|
+
#fields = { }
|
169
|
+
#fields_simple = []
|
170
|
+
#fields.merge!({ d[:x].to_time.to_i => d[:x].to_s } ); fields_simple.push(d[:x])
|
171
|
+
graph = SVG::Graph::TimeSeries.new( :height => 350, :width => 700, :show_data_labels => false, :x_label_format => "%m/%d/%y", :graph_title => self.reportable_title(pivot), :show_graph_title => true, :show_data_values => false, :show_data_points => false, :area_fill => true )
|
172
|
+
data.each do |line|
|
173
|
+
res = []
|
174
|
+
line[1].each { |d| res.push(d[:x].strftime('%m/%d/%y')).push(d[:y]) }
|
175
|
+
graph.add_data :data => res, :title => line[0].to_s
|
176
|
+
end
|
177
|
+
|
178
|
+
#logger.warn "Fields: #{fields.inspect}"
|
179
|
+
#logger.warn "Fields Simple: #{fields_simple.inspect}"
|
180
|
+
|
181
|
+
graph
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Mg::GoalMetaType < ActiveRecord::Base
|
2
|
+
set_table_name :mg_goal_meta_types
|
3
|
+
|
4
|
+
belongs_to :mg_goal, :class_name => "Mg::Goal"
|
5
|
+
has_many :gi_metas, :dependent => :destroy, :class_name => "Mg::GiMeta", :foreign_key => "mg_goal_meta_type_id"
|
6
|
+
has_many :gs_metas, :dependent => :destroy, :class_name => "Mg::GsMeta", :foreign_key => "mg_goal_meta_type_id"
|
7
|
+
|
8
|
+
validates_presence_of :name
|
9
|
+
validates_presence_of :var
|
10
|
+
validates_presence_of :meta_type
|
11
|
+
|
12
|
+
def meta
|
13
|
+
case self.meta_type
|
14
|
+
when 'gi_meta', 'ci_meta'
|
15
|
+
return self.gi_metas
|
16
|
+
when 'gs_meta', 'cs_meta'
|
17
|
+
return self.gs_metas
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class Mg::GsMeta < ActiveRecord::Base
|
2
|
+
set_table_name :mg_gs_metas
|
3
|
+
|
4
|
+
belongs_to :mg_goal_meta_type, :class_name => "Mg::GoalMetaType"
|
5
|
+
belongs_to :mg_record, :class_name => "Mg::Record"
|
6
|
+
|
7
|
+
validates_presence_of :mg_goal_meta_type_id
|
8
|
+
validates_presence_of :mg_record_id
|
9
|
+
|
10
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
class Mg::Record < ActiveRecord::Base
|
2
|
+
set_table_name :mg_records
|
3
|
+
|
4
|
+
belongs_to :mg_goal, :class_name => "Mg::Goal"
|
5
|
+
has_many :mg_goal_meta_types, :through => :mg_goal, :class_name => "Mg::GoalMetaType"
|
6
|
+
#has_many :mg_gi_metas, :through => :mg_goal_meta_types
|
7
|
+
#has_many :mg_gs_metas, :through => :mg_goal_meta_types
|
8
|
+
|
9
|
+
#Set meta data for applicable options-- don't store nil data (waste of space)
|
10
|
+
def set_meta_data(options)
|
11
|
+
options.each do |option|
|
12
|
+
gmt = self.mg_goal_meta_types.find_by_var(option[0].to_s)
|
13
|
+
|
14
|
+
#Create cmt if it doesn't current exist (unless nil)
|
15
|
+
if gmt.nil? && !option[1].nil? && ( option[1].is_a?(Integer) || option[1].is_a?(String) )
|
16
|
+
#infer type
|
17
|
+
meta_type = "gi_meta" if option[1].is_a?(Integer)
|
18
|
+
meta_type = "gs_meta" if option[1].is_a?(String)
|
19
|
+
gmt = mg_goal.mg_goal_meta_types.create!(:name => option[0].to_s, :var => option[0].to_s, :meta_type => meta_type)
|
20
|
+
end
|
21
|
+
|
22
|
+
if !gmt.nil? #only if we can do it
|
23
|
+
gmt.meta.create!(:mg_record_id => self.id, :data => option[1]) if !option[1].nil?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def meta_for( var )
|
29
|
+
gmt = self.mg_goal_meta_types.find_by_var( var.to_s )
|
30
|
+
return nil if gmt.nil?
|
31
|
+
m = gmt.meta.find_by_mg_record_id( self.id )
|
32
|
+
return nil if m.nil?
|
33
|
+
return m.data
|
34
|
+
end
|
35
|
+
|
36
|
+
def all_metas
|
37
|
+
res = {}
|
38
|
+
self.mg_goal_meta_types.each do |gmt|
|
39
|
+
r = res.count
|
40
|
+
begin
|
41
|
+
if gmt.var =~ /(\w+)[_]id/i
|
42
|
+
if Kernel.const_get($1.classify)
|
43
|
+
item = Kernel.const_get($1.classify).find( self.meta_for( gmt.var ) )
|
44
|
+
if item.respond_to?(:name)
|
45
|
+
res.merge!({ $1 => item.name })
|
46
|
+
elsif item.respond_to?(:title)
|
47
|
+
res.merge!({ $1 => item.title })
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
rescue
|
52
|
+
end
|
53
|
+
|
54
|
+
res.merge!({ gmt.var => self.meta_for( gmt.var ) }) if res.count == r
|
55
|
+
end
|
56
|
+
|
57
|
+
res
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Mg::Test represents something you are 'a/b testing'
|
2
|
+
#
|
3
|
+
# Attributes
|
4
|
+
# test_type:: Symbol uniquely identifying this test (for code interaction)
|
5
|
+
# title:: Title of the test (E.g. Banner text)
|
6
|
+
# tally_each_serve:: Should we count each view by a user as a hit, or just first-serve to that user?
|
7
|
+
# is_switch:: Are we implementing a code-switch as opposed to a text substitution
|
8
|
+
# deleted_at:: Is this test deleted? (MG Console)
|
9
|
+
# is_hidden:: Is this test hidden? (MG Console)
|
10
|
+
class Mg::Test < ActiveRecord::Base
|
11
|
+
set_table_name :mg_tests
|
12
|
+
|
13
|
+
# ActiveRecord Associations
|
14
|
+
has_many :mg_choices, :class_name => "Mg::Choice", :foreign_key => "mg_test_id"
|
15
|
+
|
16
|
+
# Validations
|
17
|
+
validates_format_of :test_type, :with => /[a-z0-9_]{3,50}/i, :message => "must be between 3 and 30 characters, alphanumeric with underscores"
|
18
|
+
validates_uniqueness_of :test_type
|
19
|
+
|
20
|
+
# Member Functions
|
21
|
+
|
22
|
+
# Get total reward of all choices for this test
|
23
|
+
def total_reward
|
24
|
+
self.mg_choices.map { |choice| choice.reward || 0 }.sum
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get total served of all choices for this test
|
28
|
+
def total_served
|
29
|
+
self.mg_choices.map { |choice| choice.served || 0 }.sum
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
/*
|
2
|
+
* g.Raphael 0.4.1 - Charting library, based on Raphaël
|
3
|
+
*
|
4
|
+
* Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
|
5
|
+
* 2011 Geoffrey Hayes (http://github.com/hayesgm)
|
6
|
+
* Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
|
7
|
+
*/
|
8
|
+
Raphael.fn.g.funnelchart = function (cx, cy, cw, ch, values, opts) {
|
9
|
+
opts = opts || {};
|
10
|
+
var paper = this,
|
11
|
+
sectors = [],
|
12
|
+
covers = this.set(),
|
13
|
+
chart = this.set(),
|
14
|
+
series = this.set(),
|
15
|
+
insetLabels = this.set(),
|
16
|
+
order = [],
|
17
|
+
len = values.length,
|
18
|
+
angle = 0,
|
19
|
+
total = 0,
|
20
|
+
others = 0,
|
21
|
+
cut = 9,
|
22
|
+
defcut = true,
|
23
|
+
max = 0.0,
|
24
|
+
sh = ch / ( len + 0.0 );
|
25
|
+
chart.covers = covers;
|
26
|
+
|
27
|
+
function trap(x0, y0, w0, w1, h) {
|
28
|
+
path = ['M', x0, y0, 'H', x0 + w0, 'L', ( w0 - w1 ) / 2.0 + x0 + w1, y0 + h, 'H', ( w0 - w1 ) / 2.0 + x0, 'Z'];
|
29
|
+
return path;
|
30
|
+
};
|
31
|
+
|
32
|
+
var labeltext;
|
33
|
+
for (var i = 0; i < len; i++) {
|
34
|
+
total += values[i];
|
35
|
+
max = max > values[i] ? max : values[i] + 0.0;
|
36
|
+
|
37
|
+
if (opts.legend && opts.legend.length > i)
|
38
|
+
labeltext = opts.legend[i];
|
39
|
+
|
40
|
+
values[i] = {value: values[i], order: i, valueOf: function () { return this.value; }, labeltext: labeltext};
|
41
|
+
}
|
42
|
+
|
43
|
+
//values.sort(function (a, b) {
|
44
|
+
// return b.value - a.value;
|
45
|
+
//});
|
46
|
+
|
47
|
+
for (i = 0; i < len; i++) {
|
48
|
+
|
49
|
+
var sx0 = ( cx + (cw / 2.0 ) - cw * ( values[i] / ( 2.0 * max ) ) );
|
50
|
+
var sy0 = cy + sh * i;
|
51
|
+
var w0 = values[i] * cw / max;
|
52
|
+
var w1 = ( i < len - 1 ) ? values[i + 1] * cw / max : 0;
|
53
|
+
|
54
|
+
if (opts.init) {
|
55
|
+
var ipath = trap(sx0, sy0, w0, w1, sh).join(",");
|
56
|
+
}
|
57
|
+
//console.log([sx0, sy0, w0, w1, sh]);
|
58
|
+
var path = trap(sx0, sy0, w0, w1, sh);
|
59
|
+
var p = this.path(path).attr({fill: opts.colors && opts.colors[i] || this.g.colors[i] || "#666", stroke: opts.stroke || "#fff", "stroke-width": (opts.strokewidth == null ? 1 : opts.strokewidth), "stroke-linejoin": "round"});
|
60
|
+
if (values[i].labeltext)
|
61
|
+
p.insetLabel = this.text(sx0 + w0 / 2.0, sy0 + sh / 3.0, values[i].labeltext + ( i > 0 ? ( " " + ( values[i] - values[i - 1] / values[i - 1] ) + "%" ) : "")).attr({font: "20px Helvetica", stroke: "#fff", fill: "#eee", "fill-opacity": 0.95, "stroke-opacity": 0 });
|
62
|
+
|
63
|
+
p.value = values[i];
|
64
|
+
sectors.push(p);
|
65
|
+
series.push(p);
|
66
|
+
opts.init && p.animate({path: path.join(",")}, (+opts.init - 1) || 1000, ">");
|
67
|
+
}
|
68
|
+
|
69
|
+
for (i = 0; i < len; i++) {
|
70
|
+
p = paper.path(sectors[i].attr("path")).attr(this.g.shim);
|
71
|
+
opts.href && opts.href[i] && p.attr({href: opts.href[i]});
|
72
|
+
p.attr = function () {};
|
73
|
+
covers.push(p);
|
74
|
+
series.push(p);
|
75
|
+
}
|
76
|
+
|
77
|
+
chart.hover = function (fin, fout) {
|
78
|
+
fout = fout || function () {};
|
79
|
+
var that = this;
|
80
|
+
for (var i = 0; i < len; i++) {
|
81
|
+
(function (sector, cover, j) {
|
82
|
+
var o = {
|
83
|
+
sector: sector,
|
84
|
+
cover: cover,
|
85
|
+
cx: cx,
|
86
|
+
cy: cy,
|
87
|
+
cw: cw,
|
88
|
+
ch: ch,
|
89
|
+
value: values[j],
|
90
|
+
total: total,
|
91
|
+
label: that.labels && that.labels[j]
|
92
|
+
};
|
93
|
+
cover.mouseover(function () {
|
94
|
+
fin.call(o);
|
95
|
+
}).mouseout(function () {
|
96
|
+
fout.call(o);
|
97
|
+
});
|
98
|
+
})(series[i], covers[i], i);
|
99
|
+
}
|
100
|
+
return this;
|
101
|
+
};
|
102
|
+
// x: where label could be put
|
103
|
+
// y: where label could be put
|
104
|
+
// value: value to show
|
105
|
+
// total: total number to count %
|
106
|
+
chart.each = function (f) {
|
107
|
+
var that = this;
|
108
|
+
for (var i = 0; i < len; i++) {
|
109
|
+
(function (sector, cover, j) {
|
110
|
+
var o = {
|
111
|
+
sector: sector,
|
112
|
+
cover: cover,
|
113
|
+
cx: cx,
|
114
|
+
cy: cy,
|
115
|
+
x: sector.middle.x,
|
116
|
+
y: sector.middle.y,
|
117
|
+
mangle: sector.mangle,
|
118
|
+
r: r,
|
119
|
+
value: values[j],
|
120
|
+
total: total,
|
121
|
+
label: that.labels && that.labels[j]
|
122
|
+
};
|
123
|
+
f.call(o);
|
124
|
+
})(series[i], covers[i], i);
|
125
|
+
}
|
126
|
+
return this;
|
127
|
+
};
|
128
|
+
chart.inject = function (element) {
|
129
|
+
element.insertBefore(covers[0]);
|
130
|
+
};
|
131
|
+
var legend = function (labels, otherslabel, mark, dir) {
|
132
|
+
var x = cx + cw * 1.7,
|
133
|
+
y = cy,
|
134
|
+
h = y + 10;
|
135
|
+
labels = labels || [];
|
136
|
+
dir = (dir && dir.toLowerCase && dir.toLowerCase()) || "east";
|
137
|
+
mark = paper.g.markers[mark && mark.toLowerCase()] || "disc";
|
138
|
+
chart.labels = paper.set();
|
139
|
+
for (var i = 0; i < len; i++) {
|
140
|
+
var clr = series[i].attr("fill"),
|
141
|
+
j = values[i].order,
|
142
|
+
txt;
|
143
|
+
values[i].others && (labels[j] = otherslabel || "Others");
|
144
|
+
labels[j] = paper.g.labelise(labels[j], values[i], total);
|
145
|
+
chart.labels.push(paper.set());
|
146
|
+
chart.labels[i].push(paper.g[mark](x + 5, h, 5).attr({fill: clr, stroke: "none"}));
|
147
|
+
chart.labels[i].push(txt = paper.text(x + 20, h, labels[j] || values[j]).attr(paper.g.txtattr).attr({fill: opts.legendcolor || "#000", "text-anchor": "start"}));
|
148
|
+
covers[i].label = chart.labels[i];
|
149
|
+
h += txt.getBBox().height * 1.2;
|
150
|
+
}
|
151
|
+
var bb = chart.labels.getBBox(),
|
152
|
+
tr = {
|
153
|
+
east: [0, -bb.height / 2],
|
154
|
+
west: [-bb.width - cw - 20, -bb.height / 2],
|
155
|
+
north: [-cw - bb.width / 2, -cw - bb.height - 10],
|
156
|
+
south: [- ( cw / 2 ) - bb.width / 2, ( cw / 2 ) + 10]
|
157
|
+
}[dir];
|
158
|
+
chart.labels.translate.apply(chart.labels, tr);
|
159
|
+
chart.push(chart.labels);
|
160
|
+
};
|
161
|
+
if (opts.legend) {
|
162
|
+
legend(opts.legend, opts.legendothers, opts.legendmark, opts.legendpos);
|
163
|
+
}
|
164
|
+
chart.push(series, covers);
|
165
|
+
chart.series = series;
|
166
|
+
chart.covers = covers;
|
167
|
+
return chart;
|
168
|
+
};
|