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.
- data/README.rdoc +46 -13
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/google_otg.gemspec +5 -2
- data/lib/google_otg.rb +505 -385
- 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
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
33
|
-
|
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
|
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
|
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-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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}&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
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
}
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
[
|
215
|
-
|
216
|
-
[
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
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}&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
|
-
|
267
|
-
|
268
|
-
|
269
|
-
now_days =
|
270
|
-
now_minutes =
|
271
|
-
now_floored =
|
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
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
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
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
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
|
-
|
301
|
-
|
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
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
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
|
-
|
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
|
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-
|
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: []
|