google_otg 1.0.19 → 1.1.0

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.
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: []