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