mountain-goat 1.0.1 → 1.0.2
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.
- 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
|
+
};
|