google_otg 1.0.19 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/README.rdoc +46 -13
  2. data/Rakefile +1 -0
  3. data/VERSION +1 -1
  4. data/google_otg.gemspec +5 -2
  5. data/lib/google_otg.rb +505 -385
  6. metadata +12 -2
data/README.rdoc CHANGED
@@ -8,7 +8,9 @@ Git:: http://github.com/esilverberg/google_otg/tree/master
8
8
  This plugin adds helpers to include Google's pretty over time line graph in your rails application. You will
9
9
  recognize this line graph from Google Analytics.
10
10
 
11
- Many features are missing, including support for multiple lines and colors. Feel free to add.
11
+ You can also download images via integration with the Google chart API as well as download CSV
12
+
13
+ See a live example at https://www.picostatus.com/reports/1J6v2eQ
12
14
 
13
15
  == Requirements
14
16
  You must be able to generate arrays of objects that respond to "count" and "created_at". The X-axis is presumed to be dates. You can control time step of the x-axis.
@@ -17,20 +19,32 @@ You must be able to generate arrays of objects that respond to "count" and "crea
17
19
  In your controller:
18
20
 
19
21
  @hits_last_week = Hits.find_by_sql(["
20
- SELECT
21
- DAYOFYEAR(hits.created_at) as d,
22
- DATE(hits.created_at) as created_at,
23
- count(*) as count
24
- FROM hits
25
- WHERE hits.created_at > UTC_TIMESTAMP() - INTERVAL 7 DAY
26
- GROUP BY d
27
- ORDER BY created_at"])
22
+ SELECT DAYOFYEAR(TIMESTAMPADD(SECOND, ?, created_at)) as d,
23
+ DATE(TIMESTAMPADD(SECOND, ?, created_at)) as created_at,
24
+ count(*) as count
25
+ FROM hits
26
+ WHERE widget_id = ?
27
+
28
+ AND created_at >= TIMESTAMPADD(SECOND, -1 * ?, DATE(?))
29
+ AND created_at <= TIMESTAMPADD(SECOND, -1 * ?, DATE(?)) + INTERVAL 1 DAY
30
+
31
+ GROUP BY d
32
+ ORDER BY created_at
33
+ ", utc_offset_in_sec, utc_offset_in_sec, widget.id, utc_offset_in_sec, lower_bound, utc_offset_in_sec, upper_bound])
28
34
 
29
35
  In your view:
30
36
 
31
37
  <%= over_time_graph(@hits_last_week) %>
32
- or
33
- <%= over_time_graph(@hits_last_week, :label => "Hits", :range => 1440, :x_label_format => "%A, %B %d", :src => "/google/OverTimeGraph.swf") %>
38
+
39
+ or
40
+
41
+ <%= over_time_graph(@hits_last_week,
42
+ :x_label_format => "%A, %B %d",
43
+ :max_x_label_count => 3,
44
+ :time_zone => @time_zone,
45
+ :range => @range,
46
+ :src => "/google/OverTimeGraph.swf") %>
47
+
34
48
 
35
49
  == +over_time_graph+
36
50
 
@@ -45,8 +59,6 @@ Some of the options available:
45
59
 
46
60
  Example:
47
61
 
48
- Some of the options available:
49
-
50
62
  google_line_graph(
51
63
  [@impressions, @conversions],
52
64
  :x_label_format => "%a, %b %d",
@@ -55,12 +67,33 @@ Some of the options available:
55
67
  :title => @company_name,
56
68
  :legend => ['Impressions','Conversions'])
57
69
 
70
+ Some of the options available:
71
+
58
72
  <tt>:title</tt>:: The title of this graph
59
73
  <tt>:legend</tt>:: The graph legend
60
74
  <tt>:title_color</tt>:: The title color
61
75
  <tt>:title_size</tt>:: Title font size
62
76
  <tt>:grid_lines</tt>:: Grid lines on the graph
63
77
 
78
+ == +data_to_csv+
79
+
80
+ Example:
81
+
82
+ data_to_csv([@impressions, @conversions],
83
+ :legend => ['Impressions','Conversions'],
84
+ :x_label_format => "%m/%d/%Y",
85
+ :time_zone => ActiveSupport::TimeZone['Hawaii'],
86
+ :range => {:lower_bound => 1.day.ago,
87
+ :upper_bound => 1.day.fromnow})
88
+
89
+ Some of the options available:
90
+
91
+ <tt>:legend</tt>:: Column headers
92
+ <tt>:time_zone</tt>:: Time zone of data
93
+ <tt>:range</tt>:: Lower & upper bound
94
+ <tt>:x_label_format</tt>:: Format string for x-axis data
95
+
96
+
64
97
  == Copyright
65
98
 
66
99
  Copyright (c) 2009 esilverberg. See LICENSE for details.
data/Rakefile CHANGED
@@ -13,6 +13,7 @@ begin
13
13
  gem.add_development_dependency "thoughtbot-shoulda"
14
14
  gem.add_dependency "mattetti-googlecharts"
15
15
  gem.add_dependency "fastercsv"
16
+ gem.add_dependency "httparty"
16
17
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
18
  end
18
19
  Jeweler::GemcutterTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.19
1
+ 1.1.0
data/google_otg.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{google_otg}
8
- s.version = "1.0.19"
8
+ s.version = "1.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["esilverberg"]
12
- s.date = %q{2009-11-08}
12
+ s.date = %q{2009-12-13}
13
13
  s.description = %q{Include Google's Over Time Graph in your app}
14
14
  s.email = %q{eric@ericsilverberg.com}
15
15
  s.extra_rdoc_files = [
@@ -48,14 +48,17 @@ Gem::Specification.new do |s|
48
48
  s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
49
49
  s.add_runtime_dependency(%q<mattetti-googlecharts>, [">= 0"])
50
50
  s.add_runtime_dependency(%q<fastercsv>, [">= 0"])
51
+ s.add_runtime_dependency(%q<httparty>, [">= 0"])
51
52
  else
52
53
  s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
53
54
  s.add_dependency(%q<mattetti-googlecharts>, [">= 0"])
54
55
  s.add_dependency(%q<fastercsv>, [">= 0"])
56
+ s.add_dependency(%q<httparty>, [">= 0"])
55
57
  end
56
58
  else
57
59
  s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
58
60
  s.add_dependency(%q<mattetti-googlecharts>, [">= 0"])
59
61
  s.add_dependency(%q<fastercsv>, [">= 0"])
62
+ s.add_dependency(%q<httparty>, [">= 0"])
60
63
  end
61
64
  end
data/lib/google_otg.rb CHANGED
@@ -1,400 +1,520 @@
1
- $:.unshift(File.dirname(__FILE__))
2
- require 'gchart_mod'
3
- require 'uri'
4
- require 'fastercsv'
5
-
6
- module GoogleOtg
7
-
8
- DEFAULT_INCREMENT = 1 # 1 day
9
-
10
- def google_line_graph(hits, args = {})
11
-
12
- raise ArgumentError, "Invalid hits" unless hits && hits.length > 0
13
-
14
- size = args.has_key?(:size) ? args[:size] : '800x200'
15
- title = args.has_key?(:title) ? args[:title] : "Graph"
16
- title_color = args.has_key?(:title_color) ? args[:title_color] : '000000'
17
- title_size = args.has_key?(:title_size) ? args[:title_size] : '20'
18
- grid_lines = args.has_key?(:grid_lines) ? args[:grid_lines] : [25,50]
19
- legend = args.has_key?(:legend) ? args[:legend] : nil
20
-
21
- x_labels = []
22
- y_labels = [0]
23
- data = []
24
-
25
- if hits[0].is_a?(Array)
26
- shape_markers = [['D','6699CC',0,'-1.0',4],['D','FF9933',1,'-1.0',2],['o','0000ff',0,'-1.0',8],['o','FF6600',1,'-1.0',8]]
27
- line_colors = ['6699CC','FF9933']
28
-
29
- hits.map{|h|
30
- converted = hits_to_gchart_range(h, args)
31
- data.push(converted[:points])
32
- x_labels = converted[:x_labels] if converted[:x_labels].length > x_labels.length
33
- y_labels = converted[:y_labels] if converted[:y_labels].max > y_labels.max
34
- }
35
-
36
- else
37
- shape_markers = [['D','6699CC',0,'-1.0',4],['o','0000ff',0,'-1.0',8]]
38
- line_colors = ['6699CC']
39
-
40
- converted = hits_to_gchart_range(hits, args)
41
- data.push(converted[:points])
42
- x_labels = converted[:x_labels]
43
- y_labels = converted[:y_labels]
44
-
45
- end
46
-
47
- axis_with_labels = 'x,y'
48
- axis_labels = [x_labels,y_labels]
49
-
50
- return Gchart.line(
51
- :size => size,
52
- :title => title,
53
- :title_color => title_color,
54
- :title_size => title_size,
55
- :grid_lines => grid_lines,
56
- :shape_markers => shape_markers,
57
- :data => data,
58
- :axis_with_labels => axis_with_labels,
59
- :max_value => y_labels[y_labels.length - 1],
60
- :legend => legend,
61
- :axis_labels => axis_labels,
62
- :line_colors => line_colors)
63
-
64
- end
65
-
66
- def data_to_csv(hits, args={})
67
- if hits.is_a?(Array) and hits[0].is_a?(Array)
68
- range = hits.map{|h| hits_to_otg_range(h, args) }
69
- else
70
- range = [hits_to_otg_range(hits, args)]
71
- end
72
-
73
- legend = []
74
- legend.concat(args[:legend])
75
-
76
- csv_data = FasterCSV.generate do |csv|
77
- x_labels = range[0][:x_labels].map{|x_label| x_label[1]}
78
- x_labels.unshift("")
79
- csv << x_labels
80
-
81
- raise ArgumentError, "Mismatched array lengths" unless range.length == args[:legend].length
82
-
83
- for data in range
84
- points = data[:points].map{|point| point[:Value][0]}
85
- points.unshift(legend.shift)
86
- csv << points
87
- end
88
- end
89
-
90
- return csv_data
91
- end
92
-
93
- def over_time_graph(hits, args = {})
94
- height = args.has_key?(:height) ? args[:height] : 125
95
- src = args.has_key?(:src) ? args[:src] : "http://www.google.com/analytics/static/flash/OverTimeGraph.swf"
96
-
97
- if hits.is_a?(Array) and hits[0].is_a?(Array)
98
- range = hits.map{|h| hits_to_otg_range(h, args) }
99
- else
100
- range = [hits_to_otg_range(hits, args)]
101
- end
102
- vars = range_to_flashvars(range)
103
-
104
- html = <<-eos
105
- <embed width="100%" height="#{height}"
106
- wmode="opaque" salign="tl" scale="noScale" quality="high" bgcolor="#FFFFFF"
107
- flashvars="input=#{vars}"
108
- pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash"
109
- src="#{src}"/>
110
- eos
111
-
112
- return html
113
-
114
- end
115
-
116
- def google_pie(hits, label_fn, args = {})
117
- height = args.has_key?(:height) ? args[:height] : 125
118
- width = args.has_key?(:width) ? args[:width] : 125
119
- pie_values = extract_pct_values(hits, label_fn, args)
120
- vars = pie_to_flashvars(pie_values, args)
121
- src = args.has_key?(:src) ? args[:src] : "http://www.google.com/analytics/static/flash/pie.swf"
122
-
123
- html = <<-eos
124
- <embed
125
- width="#{width}"
126
- height="#{height}"
127
- salign="tl"
128
- scale="noScale"
129
- quality="high"
130
- bgcolor="#FFFFFF"
131
- flashvars="input=#{vars}&amp;locale=en-US"
132
- pluginspage="http://www.macromedia.com/go/getflashplayer"
133
- type="application/x-shockwave-flash"
1
+ $:.unshift(File.dirname(__FILE__))
2
+ require 'gchart_mod'
3
+ require 'uri'
4
+ require 'fastercsv'
5
+ require 'httparty'
6
+
7
+ module GoogleOtg
8
+ include HTTParty
9
+ @@DEFAULT_INCREMENT = 1 # 1 day
10
+
11
+ module Helper
12
+ def date_range
13
+ tr = self.setup_time_range
14
+ tr[:lower_bound].strftime("%m/%d/%Y") + " - " + tr[:upper_bound].strftime("%m/%d/%Y")
15
+ end
16
+
17
+ def over_time_graph(hits, args = {})
18
+ tr = self.setup_time_range
19
+ args[:time_zone] = tr[:time_zone] unless args.has_key?(:time_zone)
20
+ args[:range] = tr[:range] unless args.has_key?(:range)
21
+
22
+ height = args.has_key?(:height) ? args[:height] : 125
23
+ src = args.has_key?(:src) ? args[:src] : "http://www.google.com/analytics/static/flash/OverTimeGraph.swf"
24
+
25
+ if hits.is_a?(Array) and hits[0].is_a?(Array)
26
+ range = hits.map{|h| hits_to_otg_range(h, args) }
27
+ else
28
+ range = [hits_to_otg_range(hits, args)]
29
+ end
30
+ vars = range_to_flashvars(range)
31
+
32
+ html = <<-eos
33
+ <embed width="100%" height="#{height}"
34
+ wmode="opaque" salign="tl" scale="noScale" quality="high" bgcolor="#FFFFFF"
35
+ flashvars="input=#{vars}"
36
+ pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash"
134
37
  src="#{src}"/>
135
- eos
136
- return html
137
-
138
- end
139
-
140
- def pie_to_flashvars(args = {})
141
-
142
- labels = args[:labels]
143
- raw_values = args[:raw_values]
144
- percent_values = args[:percent_values]
145
-
146
- options = {
147
- :Pie => {
148
- :Id => "Pie",
149
- :Compare => false,
150
- :HasOtherSlice => false,
151
- :RawValues => raw_values,
152
- :Format => "DASHBOARD",
153
- :PercentValues => percent_values
154
- }
155
- }
156
-
157
- return URI::encode(options.to_json)
158
-
159
- end
160
- protected :pie_to_flashvars
161
-
162
- def extract_pct_values(hits, label_fn, args = {})
163
-
164
- limit = args.has_key?(:limit) ? args[:limit] : 0.0
165
-
166
- total = 0.0
167
- other = 0.0
168
- percent_values = []
169
- raw_values = []
170
- labels = []
171
- values = []
172
- hits.each{|hit|
173
- total += hit.count.to_f
174
- }
175
- hits.each{|hit|
176
- ct = hit.count.to_f
177
- pct = (ct / total)
178
-
179
- if pct > limit
180
- percent_values.push([pct, sprintf("%.2f%%", pct * 100)])
181
- raw_values.push([ct, ct])
182
-
183
- label = label_fn.call(hit)
184
- meta = args.has_key?(:meta) ? args[:meta].call(hit) : nil
185
-
186
- labels.push(label)
187
- values.push({:label => label, :meta => meta, :percent_value => [pct, sprintf("%.2f%%", pct * 100)], :raw_value => ct})
38
+ eos
39
+
40
+ return html
41
+
42
+ end
43
+ end
44
+
45
+ def over_time_graph_download(results, type, title, legend)
46
+ tr = self.setup_time_range
47
+
48
+ if type == :csv
49
+ csv_data = data_to_csv(results,
50
+ :legend => legend,
51
+ :x_label_format => "%m/%d/%Y",
52
+ :time_zone => tr[:time_zone],
53
+ :range => tr[:range])
54
+
55
+ time_label = tr[:time_zone].now.strftime("%Y-%m-%d")
56
+ file_data_type = legend.join(" ").downcase.gsub(" ", "_")
57
+ outfile = "#{request.env['HTTP_HOST']}-#{file_data_type}_#{time_label}.csv"
58
+
59
+ send_data csv_data,
60
+ :type => 'text/csv; charset=iso-8859-1; header=present',
61
+ :disposition => "attachment; filename=#{outfile}"
62
+ else
63
+ graph_url = google_line_graph(
64
+ results ,
65
+ :x_label_format => "%a, %b %d",
66
+ :time_zone => tr[:time_zone],
67
+ :title => title,
68
+ :range => tr[:range],
69
+ :max_x_label_count => 4,
70
+ :legend => legend)
71
+
72
+ if params.has_key?(:send_file)
73
+ send_data HTTParty::get(graph_url), :filename => "graph.png"
74
+
188
75
  else
189
- other += ct
76
+ url = url_for({:send_file => 1,
77
+ :controller => controller_name,
78
+ :action => action_name}.merge(params))
79
+ render :inline => "<img src='#{url}'/>"
190
80
  end
191
- }
192
- if other > 0.0
193
- pct = other / total
194
- percent_values.push([pct, sprintf("%.2f%%", pct * 100)])
195
- raw_values.push([other, other])
196
- labels.push("Other")
197
- values.push({:label => "Other", :percent_value => [pct, sprintf("%.2f%%", pct * 100)], :raw_value => other})
81
+ end
82
+ end
83
+
84
+ def grouped_query(class_to_query, args = [])
85
+
86
+ date_field = args.has_key?(:date_field) ? args[:date_field] : "created_at"
87
+ where_args = args.has_key?(:conditions) ? args[:conditions] : ["TRUE"]
88
+ where_query = where_args.shift
89
+
90
+ tr = self.setup_time_range
91
+ return class_to_query.find_by_sql(["
92
+ SELECT DAYOFYEAR(TIMESTAMPADD(SECOND, ?, #{date_field})) as d,
93
+ DATE(TIMESTAMPADD(SECOND, ?, #{date_field})) as created_at,
94
+ count(*) as count
95
+ FROM #{class_to_query.table_name}
96
+ WHERE #{where_query}
97
+
98
+ AND #{date_field} >= TIMESTAMPADD(SECOND, -1 * ?, DATE(?))
99
+ AND #{date_field} <= TIMESTAMPADD(SECOND, -1 * ?, DATE(?)) + INTERVAL 1 DAY
100
+
101
+ GROUP BY d
102
+ ORDER BY created_at
103
+ ", tr[:time_zone].utc_offset,
104
+ tr[:time_zone].utc_offset,
105
+ where_args,
106
+ tr[:time_zone].utc_offset,
107
+ tr[:lower_bound].strftime("%Y-%m-%d"),
108
+ tr[:time_zone].utc_offset,
109
+ tr[:upper_bound].strftime("%Y-%m-%d")].flatten)
198
110
  end
199
111
 
200
- return {:labels => labels, :raw_values => raw_values, :percent_values => percent_values, :values => values}
201
-
202
- end
203
- protected :extract_pct_values
204
-
205
- def flto10(val)
206
- return ((val / 10) * 10).to_i
207
- end
208
- protected :flto10
209
-
210
- def hits_to_otg_range(hits, args = {})
211
- return hits_to_range(hits, lambda {|count, date_key, date_value|
212
- {:Value => [count, count], :Label => [date_key, date_value]}
213
- }, lambda{|mid, top|
214
- [[mid,mid],[top,top]]
215
- }, lambda{|hit, hit_date_key, hit_date_value|
216
- [hit_date_key, hit_date_value]
217
- },args)
218
- end
219
-
220
- def hits_to_gchart_range(hits, args = {})
221
- return hits_to_range(hits, lambda {|count, date_key, date_value|
222
- count
223
- }, lambda {|mid, top|
224
- [0,top/2,top]
225
- },lambda{|hit, hit_date_key, hit_date_value|
226
- hit_date_value
227
- }, args)
228
- end
229
-
230
- def hits_to_range(hits, points_fn, y_label_fn, x_label_fn, args = {})
231
-
232
- return nil unless hits
233
-
234
- hits.map{|h|
235
- if !h.respond_to?("created_at") || !h.respond_to?("count")
236
- raise ArgumentError, "Invalid object type. All objects must respond to 'count' and 'created_at'"
112
+ protected
113
+ def google_line_graph(hits, args = {})
114
+
115
+ raise ArgumentError, "Invalid hits" unless hits
116
+
117
+ size = args.has_key?(:size) ? args[:size] : '800x200'
118
+ title = args.has_key?(:title) ? args[:title] : "Graph"
119
+ title_color = args.has_key?(:title_color) ? args[:title_color] : '000000'
120
+ title_size = args.has_key?(:title_size) ? args[:title_size] : '20'
121
+ grid_lines = args.has_key?(:grid_lines) ? args[:grid_lines] : [25,50]
122
+ legend = args.has_key?(:legend) ? args[:legend] : nil
123
+
124
+ x_labels = []
125
+ y_labels = [0]
126
+ data = []
127
+
128
+ if hits[0].is_a?(Array)
129
+ shape_markers = [['D','6699CC',0,'-1.0',4],['D','FF9933',1,'-1.0',2],['o','0000ff',0,'-1.0',8],['o','FF6600',1,'-1.0',8]]
130
+ line_colors = ['6699CC','FF9933']
131
+
132
+ hits.map{|h|
133
+ converted = hits_to_gchart_range(h, args)
134
+ data.push(converted[:points])
135
+ x_labels = converted[:x_labels] if converted[:x_labels].length > x_labels.length
136
+ y_labels = converted[:y_labels] if converted[:y_labels].max > y_labels.max
137
+ }
138
+
139
+ else
140
+ shape_markers = [['D','6699CC',0,'-1.0',4],['o','0000ff',0,'-1.0',8]]
141
+ line_colors = ['6699CC']
142
+
143
+ converted = hits_to_gchart_range(hits, args)
144
+ data.push(converted[:points])
145
+ x_labels = converted[:x_labels]
146
+ y_labels = converted[:y_labels]
147
+
148
+ end
149
+
150
+ axis_with_labels = 'x,y'
151
+ axis_labels = [x_labels,y_labels]
152
+
153
+ return Gchart.line(
154
+ :size => size,
155
+ :title => title,
156
+ :title_color => title_color,
157
+ :title_size => title_size,
158
+ :grid_lines => grid_lines,
159
+ :shape_markers => shape_markers,
160
+ :data => data,
161
+ :axis_with_labels => axis_with_labels,
162
+ :max_value => y_labels[y_labels.length - 1],
163
+ :legend => legend,
164
+ :axis_labels => axis_labels,
165
+ :line_colors => line_colors)
166
+
167
+ end
168
+
169
+ def data_to_csv(hits, args={})
170
+ if hits.is_a?(Array) and hits[0].is_a?(Array)
171
+ range = hits.map{|h| hits_to_otg_range(h, args) }
172
+ else
173
+ range = [hits_to_otg_range(hits, args)]
237
174
  end
238
- }
239
-
240
- tz = args.has_key?(:time_zone) ? args[:time_zone] : ActiveSupport::TimeZone['UTC']
241
- label = args.has_key?(:label) ? args[:label] : "Value"
242
- time_fn = args.has_key?(:time_fn) ? args[:time_fn] : lambda {|h|
243
- return tz.local(h.created_at.year, h.created_at.month, h.created_at.day, h.created_at.hour, h.created_at.min, h.created_at.sec) # create zoned time
244
- }
245
- increment = args.has_key?(:increment) ? args[:increment] : DEFAULT_INCREMENT
246
-
247
- x_label_format = args.has_key?(:x_label_format) ? args[:x_label_format] : "%A %I:%M%p"
248
-
249
- max_y = 0
250
- hits_dict = {}
251
- hits.each { |h|
252
- hits_dict[time_fn.call(h)] = h
253
- }
254
-
255
- total = 0
256
-
257
- points = []
258
- point_dates = []
259
-
260
- if args[:range] && args[:range][:lower_bound]
261
- current = args[:range][:lower_bound]
262
- else
263
- current = hits.length > 0 ? time_fn.call(hits[0]) : now_floored
175
+
176
+ legend = []
177
+ legend.concat(args[:legend])
178
+
179
+ csv_data = FasterCSV.generate do |csv|
180
+ x_labels = range[0][:x_labels].map{|x_label| x_label[1]}
181
+ x_labels.unshift("")
182
+ csv << x_labels
183
+
184
+ raise ArgumentError, "Mismatched array lengths" unless range.length == args[:legend].length
185
+
186
+ for data in range
187
+ points = data[:points].map{|point| point[:Value][0]}
188
+ points.unshift(legend.shift)
189
+ csv << points
190
+ end
191
+ end
192
+
193
+ return csv_data
194
+ end
195
+
196
+ def google_pie(hits, label_fn, args = {})
197
+ height = args.has_key?(:height) ? args[:height] : 125
198
+ width = args.has_key?(:width) ? args[:width] : 125
199
+ pie_values = extract_pct_values(hits, label_fn, args)
200
+ vars = pie_to_flashvars(pie_values, args)
201
+ src = args.has_key?(:src) ? args[:src] : "http://www.google.com/analytics/static/flash/pie.swf"
202
+
203
+ html = <<-eos
204
+ <embed
205
+ width="#{width}"
206
+ height="#{height}"
207
+ salign="tl"
208
+ scale="noScale"
209
+ quality="high"
210
+ bgcolor="#FFFFFF"
211
+ flashvars="input=#{vars}&amp;locale=en-US"
212
+ pluginspage="http://www.macromedia.com/go/getflashplayer"
213
+ type="application/x-shockwave-flash"
214
+ src="#{src}"/>
215
+ eos
216
+ return html
217
+
218
+ end
219
+
220
+ # override this if you have a better way of retrieving time zones
221
+ def get_time_zone
222
+ return defined?(current_user) && !current_user.nil? && current_user.responds_to?(:time_zone) ? current_user.time_zone : ActiveSupport::TimeZone["UTC"]
264
223
  end
265
-
266
- if args[:range] && args[:range][:upper_bound]
267
- now_floored = args[:range][:upper_bound]
268
- else
269
- now_days = tz.now # use this get the right year, month and day
270
- now_minutes = tz.at((now_days.to_i/(60*(increment * 1440)))*(60*(increment * 1440))).gmtime
271
- now_floored = tz.local(now_days.year, now_days.month, now_days.day,
224
+
225
+ def setup_time_range()
226
+ time_zone = get_time_zone
227
+
228
+ now_days = time_zone.now # use this get the right year, month and day
229
+ now_minutes = time_zone.at((now_days.to_i/(60*(1 * 1440)))*(60*(1 * 1440))).gmtime
230
+ now_floored = time_zone.local(now_days.year, now_days.month, now_days.day,
272
231
  now_minutes.hour, now_minutes.min, now_minutes.sec)
232
+
233
+ default_upper = now_floored
234
+ default_lower = default_upper - 7.days
235
+
236
+ lower_bound = nil
237
+ upper_bound = nil
238
+
239
+ if params[:range]
240
+ dates = params[:range].split("-")
241
+ sql_dates = dates.map{|d|
242
+ time_zone.parse(d)
243
+ }
244
+ if sql_dates.length == 2
245
+ lower_bound = sql_dates[0]
246
+ upper_bound = sql_dates[1]
247
+ elsif sql_dates.length == 1
248
+
249
+ lower_bound = sql_dates[0]
250
+ upper_bound = default_upper
251
+ end
252
+ end
253
+
254
+ lower_bound = default_lower unless lower_bound && lower_bound < upper_bound
255
+ upper_bound = default_upper unless upper_bound && upper_bound < Time.now + 1.day
256
+
257
+ range = {:lower_bound => lower_bound, :upper_bound => upper_bound}
258
+ return {:time_zone => time_zone, :lower_bound => lower_bound, :upper_bound => upper_bound, :range => range}
273
259
  end
274
260
 
275
- while (current < now_floored + increment.days && increment > 0) do
276
- if hits_dict[current]
277
- count = hits_dict[current].count.to_i
278
- max_y = count if count > max_y
279
-
280
- date = time_fn.call(hits_dict[current])
281
- date_key = date.to_i
282
- date_value = date.strftime(x_label_format)
283
-
284
- points.push(points_fn.call(count, date_key, date_value))
285
- total += count
286
- else
287
-
288
- date = current
289
- date_key = date.to_i
290
- date_value = date.strftime(x_label_format)
291
-
292
- points.push(points_fn.call(0, date_key, date_value))
261
+ def pie_to_flashvars(args = {})
262
+
263
+ labels = args[:labels]
264
+ raw_values = args[:raw_values]
265
+ percent_values = args[:percent_values]
266
+
267
+ options = {
268
+ :Pie => {
269
+ :Id => "Pie",
270
+ :Compare => false,
271
+ :HasOtherSlice => false,
272
+ :RawValues => raw_values,
273
+ :Format => "DASHBOARD",
274
+ :PercentValues => percent_values
275
+ }
276
+ }
277
+
278
+ return URI::encode(options.to_json)
279
+
280
+ end
281
+
282
+ def extract_pct_values(hits, label_fn, args = {})
283
+
284
+ limit = args.has_key?(:limit) ? args[:limit] : 0.0
285
+
286
+ total = 0.0
287
+ other = 0.0
288
+ percent_values = []
289
+ raw_values = []
290
+ labels = []
291
+ values = []
292
+ hits.each{|hit|
293
+ total += hit.count.to_f
294
+ }
295
+ hits.each{|hit|
296
+ ct = hit.count.to_f
297
+ pct = (ct / total)
298
+
299
+ if pct > limit
300
+ percent_values.push([pct, sprintf("%.2f%%", pct * 100)])
301
+ raw_values.push([ct, ct])
302
+
303
+ label = label_fn.call(hit)
304
+ meta = args.has_key?(:meta) ? args[:meta].call(hit) : nil
305
+
306
+ labels.push(label)
307
+ values.push({:label => label, :meta => meta, :percent_value => [pct, sprintf("%.2f%%", pct * 100)], :raw_value => ct})
308
+ else
309
+ other += ct
310
+ end
311
+ }
312
+ if other > 0.0
313
+ pct = other / total
314
+ percent_values.push([pct, sprintf("%.2f%%", pct * 100)])
315
+ raw_values.push([other, other])
316
+ labels.push("Other")
317
+ values.push({:label => "Other", :percent_value => [pct, sprintf("%.2f%%", pct * 100)], :raw_value => other})
293
318
  end
294
- # Save the date for the x labels later
295
- point_dates.push({:key => date_key, :value => date_value})
296
- current = current + increment.days
297
- break if points.length > 365 # way too long dudes - no data fetching over 1 yr
319
+
320
+ return {:labels => labels, :raw_values => raw_values, :percent_values => percent_values, :values => values}
321
+
322
+ end
323
+
324
+ def flto10(val)
325
+ return ((val / 10) * 10).to_i
326
+ end
327
+
328
+ def hits_to_otg_range(hits, args = {})
329
+ return hits_to_range(hits, lambda {|count, date_key, date_value|
330
+ {:Value => [count, count], :Label => [date_key, date_value]}
331
+ }, lambda{|mid, top|
332
+ [[mid,mid],[top,top]]
333
+ }, lambda{|hit, hit_date_key, hit_date_value|
334
+ [hit_date_key, hit_date_value]
335
+ },args)
298
336
  end
299
-
300
- if points.length > 100
301
- points = points[points.length - 100..points.length - 1]
337
+
338
+ def hits_to_gchart_range(hits, args = {})
339
+ return hits_to_range(hits, lambda {|count, date_key, date_value|
340
+ count
341
+ }, lambda {|mid, top|
342
+ [0,top/2,top]
343
+ },lambda{|hit, hit_date_key, hit_date_value|
344
+ hit_date_value
345
+ }, args)
302
346
  end
303
-
304
- ## Setup Y axis labels ##
305
- max_y = args.has_key?(:max_y) ? (args[:max_y] > max_y ? args[:max_y] : max_y) : max_y
306
-
307
- top_y = self.flto10(max_y) + 10
308
- mid_y = self.flto10(top_y / 2)
309
- y_labels = y_label_fn.call(mid_y, top_y)
310
- ## end y axis labels ##
311
-
312
- ## Setup X axis labels
313
- x_labels = []
314
- max_x_label_count = args.has_key?(:max_x_label_count) ? args[:max_x_label_count] : points.length
315
-
316
- if points.length > 0
317
- step = [points.length / max_x_label_count, 1].max
318
- idx = 0
319
-
320
- while idx < points.length
321
- point = points[idx]
322
- date = point_dates[idx]
323
- x_labels.push(x_label_fn.call(point, date[:key], date[:value]))
324
- idx += step
347
+
348
+ def hits_to_range(hits, points_fn, y_label_fn, x_label_fn, args = {})
349
+
350
+ return nil unless hits
351
+
352
+ hits.map{|h|
353
+ if !h.respond_to?("created_at") || !h.respond_to?("count")
354
+ raise ArgumentError, "Invalid object type. All objects must respond to 'count' and 'created_at'"
355
+ end
356
+ }
357
+
358
+ tz = args.has_key?(:time_zone) ? args[:time_zone] : ActiveSupport::TimeZone['UTC']
359
+ label = args.has_key?(:label) ? args[:label] : "Value"
360
+ time_fn = args.has_key?(:time_fn) ? args[:time_fn] : lambda {|h|
361
+ return tz.parse(h.created_at) if h.created_at.class == String
362
+ return tz.local(h.created_at.year, h.created_at.month, h.created_at.day, h.created_at.hour, h.created_at.min, h.created_at.sec) # create zoned time
363
+ }
364
+ increment = args.has_key?(:increment) ? args[:increment] : @@DEFAULT_INCREMENT
365
+
366
+ x_label_format = args.has_key?(:x_label_format) ? args[:x_label_format] : "%A, %B %d"
367
+
368
+ max_y = 0
369
+ hits_dict = {}
370
+ hits.each { |h|
371
+ hits_dict[time_fn.call(h)] = h
372
+ }
373
+
374
+ total = 0
375
+
376
+ points = []
377
+ point_dates = []
378
+
379
+ if args[:range] && args[:range][:lower_bound]
380
+ current = args[:range][:lower_bound]
381
+ else
382
+ current = hits.length > 0 ? time_fn.call(hits[0]) : now_floored
383
+ end
384
+
385
+ if args[:range] && args[:range][:upper_bound]
386
+ now_floored = args[:range][:upper_bound]
387
+ else
388
+ now_days = tz.now # use this get the right year, month and day
389
+ now_minutes = tz.at((now_days.to_i/(60*(increment * 1440)))*(60*(increment * 1440))).gmtime
390
+ now_floored = tz.local(now_days.year, now_days.month, now_days.day,
391
+ now_minutes.hour, now_minutes.min, now_minutes.sec)
392
+ end
393
+
394
+ while (current < now_floored + increment.days && increment > 0) do
395
+ if hits_dict[current]
396
+ count = hits_dict[current].count.to_i
397
+ max_y = count if count > max_y
398
+
399
+ date = time_fn.call(hits_dict[current])
400
+ date_key = date.to_i
401
+ date_value = date.strftime(x_label_format)
402
+
403
+ points.push(points_fn.call(count, date_key, date_value))
404
+ total += count
405
+ else
406
+
407
+ date = current
408
+ date_key = date.to_i
409
+ date_value = date.strftime(x_label_format)
410
+
411
+ points.push(points_fn.call(0, date_key, date_value))
412
+ end
413
+ # Save the date for the x labels later
414
+ point_dates.push({:key => date_key, :value => date_value})
415
+ current = current + increment.days
416
+ break if points.length > 365 # way too long dudes - no data fetching over 1 yr
417
+ end
418
+
419
+ if points.length > 100
420
+ points = points[points.length - 100..points.length - 1]
421
+ end
422
+
423
+ ## Setup Y axis labels ##
424
+ max_y = args.has_key?(:max_y) ? (args[:max_y] > max_y ? args[:max_y] : max_y) : max_y
425
+
426
+ top_y = self.flto10(max_y) + 10
427
+ mid_y = self.flto10(top_y / 2)
428
+ y_labels = y_label_fn.call(mid_y, top_y)
429
+ ## end y axis labels ##
430
+
431
+ ## Setup X axis labels
432
+ x_labels = []
433
+ max_x_label_count = args.has_key?(:max_x_label_count) ? args[:max_x_label_count] : points.length
434
+
435
+ if points.length > 0
436
+ step = [points.length / max_x_label_count, 1].max
437
+ idx = 0
438
+
439
+ while idx < points.length
440
+ if idx + step >= points.length
441
+ idx = points.length - 1
442
+ end
443
+
444
+ point = points[idx]
445
+ date = point_dates[idx]
446
+ x_labels.push(x_label_fn.call(point, date[:key], date[:value]))
447
+
448
+ idx += step
449
+ end
325
450
  end
451
+
452
+ ## End x axis labels ##
453
+
454
+ return {:x_labels => x_labels, :y_labels => y_labels, :label => label, :points => points, :total => total}
455
+
456
+ end
457
+
458
+ PRIMARY_STYLE = {
459
+ :PointShape => "CIRCLE",
460
+ :PointRadius => 9,
461
+ :FillColor => 30668,
462
+ :FillAlpha => 10,
463
+ :LineThickness => 4,
464
+ :ActiveColor => 30668,
465
+ :InactiveColor => 11654895
466
+ }
467
+
468
+ COMPARE_STYLE = {
469
+ :PointShape => "CIRCLE",
470
+ :PointRadius => 6,
471
+ :FillAlpha => 10,
472
+ :LineThickness => 2,
473
+ :ActiveColor => 16750848,
474
+ :InactiveColor => 16750848
475
+ }
476
+
477
+ def setup_series(args, style)
478
+ raise ArgumentError unless args[:label]
479
+ raise ArgumentError unless args[:points]
480
+ raise ArgumentError unless args[:y_labels]
481
+
482
+ return {
483
+ :SelectionStartIndex => 0,
484
+ :SelectionEndIndex => args[:points].length,
485
+ :Style => style,
486
+ :Label => args[:label],
487
+ :Id => "primary",
488
+ :YLabels => args[:y_labels],
489
+ :ValueCategory => "visits",
490
+ :Points => args[:points]
491
+ } # end graph
492
+ end
493
+
494
+ def range_to_flashvars(args = {})
495
+ raise ArgumentError unless args.length > 0
496
+ x_labels = args[0][:x_labels]
497
+ raise ArgumentError unless x_labels
498
+
499
+ ct = 0
500
+ # this is the structure necessary to support the Google Analytics OTG
501
+
502
+ options = {:Graph => {
503
+ :Id => "Graph",
504
+ :ShowHover => true,
505
+ :Format => "NORMAL",
506
+ :XAxisTitle => "Day",
507
+ :Compare => false,
508
+ :XAxisLabels => x_labels,
509
+ :HoverType => "primary_compare",
510
+ :SelectedSeries => ["primary", "compare"],
511
+ :Series => args.map {|arg|
512
+ ct += 1
513
+ setup_series(arg, ct == 1 ? PRIMARY_STYLE : COMPARE_STYLE)
514
+ }
515
+ } # end graph
516
+ } # end options
517
+ return URI::encode(options.to_json)
326
518
  end
327
-
328
- ## End x axis labels ##
329
-
330
- return {:x_labels => x_labels, :y_labels => y_labels, :label => label, :points => points, :total => total}
331
-
332
- end
333
- protected :hits_to_range
334
-
335
-
336
- PRIMARY_STYLE = {
337
- :PointShape => "CIRCLE",
338
- :PointRadius => 9,
339
- :FillColor => 30668,
340
- :FillAlpha => 10,
341
- :LineThickness => 4,
342
- :ActiveColor => 30668,
343
- :InactiveColor => 11654895
344
- }
345
-
346
- COMPARE_STYLE = {
347
- :PointShape => "CIRCLE",
348
- :PointRadius => 6,
349
- :FillAlpha => 10,
350
- :LineThickness => 2,
351
- :ActiveColor => 16750848,
352
- :InactiveColor => 16750848
353
- }
354
-
355
- def setup_series(args, style)
356
- raise ArgumentError unless args[:label]
357
- raise ArgumentError unless args[:points]
358
- raise ArgumentError unless args[:y_labels]
359
-
360
- return {
361
- :SelectionStartIndex => 0,
362
- :SelectionEndIndex => args[:points].length,
363
- :Style => style,
364
- :Label => args[:label],
365
- :Id => "primary",
366
- :YLabels => args[:y_labels],
367
- :ValueCategory => "visits",
368
- :Points => args[:points]
369
- } # end graph
370
- end
371
- protected :setup_series
372
-
373
- def range_to_flashvars(args = {})
374
- raise ArgumentError unless args.length > 0
375
- x_labels = args[0][:x_labels]
376
- raise ArgumentError unless x_labels
377
-
378
- ct = 0
379
- # this is the structure necessary to support the Google Analytics OTG
380
-
381
- options = {:Graph => {
382
- :Id => "Graph",
383
- :ShowHover => true,
384
- :Format => "NORMAL",
385
- :XAxisTitle => "Day",
386
- :Compare => false,
387
- :XAxisLabels => x_labels,
388
- :HoverType => "primary_compare",
389
- :SelectedSeries => ["primary", "compare"],
390
- :Series => args.map {|arg|
391
- ct += 1
392
- setup_series(arg, ct == 1 ? PRIMARY_STYLE : COMPARE_STYLE)
393
- }
394
- } # end graph
395
- } # end options
396
- return URI::encode(options.to_json)
397
- end
398
- protected :range_to_flashvars
399
-
400
- end
519
+ end
520
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: google_otg
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.19
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - esilverberg
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-08 00:00:00 -08:00
12
+ date: 2009-12-13 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -42,6 +42,16 @@ dependencies:
42
42
  - !ruby/object:Gem::Version
43
43
  version: "0"
44
44
  version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: httparty
47
+ type: :runtime
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
45
55
  description: Include Google's Over Time Graph in your app
46
56
  email: eric@ericsilverberg.com
47
57
  executables: []