google_otg 1.0.9
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/.document +5 -0
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +66 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/google_otg.gemspec +58 -0
- data/lib/gchart_mod.rb +105 -0
- data/lib/google_otg.rb +337 -0
- data/rails/init.rb +5 -0
- data/test/google_otg_test.rb +52 -0
- data/test/test_helper.rb +42 -0
- metadata +87 -0
data/.document
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 esilverberg
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
= google_otg
|
2
|
+
|
3
|
+
Author:: Eric Silverberg (http://www.ericsilverberg.com)
|
4
|
+
Copyright:: Copyright (c) 2009 Eric Silverberg
|
5
|
+
License:: MIT (Go Beavers!)
|
6
|
+
Git:: http://github.com/esilverberg/google_otg/tree/master
|
7
|
+
|
8
|
+
This plugin adds helpers to include Google's pretty over time line graph in your rails application. You will
|
9
|
+
recognize this line graph from Google Analytics.
|
10
|
+
|
11
|
+
Many features are missing, including support for multiple lines and colors. Feel free to add.
|
12
|
+
|
13
|
+
== Requirements
|
14
|
+
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.
|
15
|
+
|
16
|
+
== Example Usage
|
17
|
+
In your controller:
|
18
|
+
|
19
|
+
@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"])
|
28
|
+
|
29
|
+
In your view:
|
30
|
+
|
31
|
+
<%= 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") %>
|
34
|
+
|
35
|
+
== +over_time_graph+
|
36
|
+
|
37
|
+
Some of the options available:
|
38
|
+
|
39
|
+
<tt>:label</tt>:: The label of quantity being measured
|
40
|
+
<tt>:range</tt>:: The time step, in minutes
|
41
|
+
<tt>:x_label_format</tt>:: The time format for the x label
|
42
|
+
<tt>:src</tt>:: An optional local source to serve this widget (otherwise will serve from Google)
|
43
|
+
|
44
|
+
== +google_line_graph+
|
45
|
+
|
46
|
+
Example:
|
47
|
+
|
48
|
+
Some of the options available:
|
49
|
+
|
50
|
+
google_line_graph(
|
51
|
+
[@impressions, @conversions],
|
52
|
+
:x_label_format => "%a, %b %d",
|
53
|
+
:time_zone => @time_zone,
|
54
|
+
:range => 1440,
|
55
|
+
:title => @company_name,
|
56
|
+
:legend => ['Impressions','Conversions'])
|
57
|
+
|
58
|
+
<tt>:title</tt>:: The title of this graph
|
59
|
+
<tt>:legend</tt>:: The graph legend
|
60
|
+
<tt>:title_color</tt>:: The title color
|
61
|
+
<tt>:title_size</tt>:: Title font size
|
62
|
+
<tt>:grid_lines</tt>:: Grid lines on the graph
|
63
|
+
|
64
|
+
== Copyright
|
65
|
+
|
66
|
+
Copyright (c) 2009 esilverberg. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "google_otg"
|
8
|
+
gem.summary = %Q{Google's amazing over-time graph, in your rails app}
|
9
|
+
gem.description = %Q{Include Google's Over Time Graph in your app}
|
10
|
+
gem.email = "eric@ericsilverberg.com"
|
11
|
+
gem.homepage = "http://github.com/esilverberg/google_otg"
|
12
|
+
gem.authors = ["esilverberg"]
|
13
|
+
gem.add_development_dependency "thoughtbot-shoulda"
|
14
|
+
gem.add_dependency "mattetti-googlecharts"
|
15
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
Rake::TestTask.new(:test) do |test|
|
24
|
+
test.libs << 'lib' << 'test'
|
25
|
+
test.pattern = 'test/**/*_test.rb'
|
26
|
+
test.verbose = true
|
27
|
+
end
|
28
|
+
|
29
|
+
begin
|
30
|
+
require 'rcov/rcovtask'
|
31
|
+
Rcov::RcovTask.new do |test|
|
32
|
+
test.libs << 'test'
|
33
|
+
test.pattern = 'test/**/*_test.rb'
|
34
|
+
test.verbose = true
|
35
|
+
end
|
36
|
+
rescue LoadError
|
37
|
+
task :rcov do
|
38
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
task :test => :check_dependencies
|
43
|
+
|
44
|
+
task :default => :test
|
45
|
+
|
46
|
+
require 'rake/rdoctask'
|
47
|
+
Rake::RDocTask.new do |rdoc|
|
48
|
+
if File.exist?('VERSION')
|
49
|
+
version = File.read('VERSION')
|
50
|
+
else
|
51
|
+
version = ""
|
52
|
+
end
|
53
|
+
|
54
|
+
rdoc.rdoc_dir = 'rdoc'
|
55
|
+
rdoc.title = "google_otg #{version}"
|
56
|
+
rdoc.rdoc_files.include('README*')
|
57
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
58
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.9
|
data/google_otg.gemspec
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{google_otg}
|
8
|
+
s.version = "1.0.9"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["esilverberg"]
|
12
|
+
s.date = %q{2009-10-01}
|
13
|
+
s.description = %q{Include Google's Over Time Graph in your app}
|
14
|
+
s.email = %q{eric@ericsilverberg.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"google_otg.gemspec",
|
27
|
+
"lib/gchart_mod.rb",
|
28
|
+
"lib/google_otg.rb",
|
29
|
+
"rails/init.rb",
|
30
|
+
"test/google_otg_test.rb",
|
31
|
+
"test/test_helper.rb"
|
32
|
+
]
|
33
|
+
s.homepage = %q{http://github.com/esilverberg/google_otg}
|
34
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
35
|
+
s.require_paths = ["lib"]
|
36
|
+
s.rubygems_version = %q{1.3.5}
|
37
|
+
s.summary = %q{Google's amazing over-time graph, in your rails app}
|
38
|
+
s.test_files = [
|
39
|
+
"test/google_otg_test.rb",
|
40
|
+
"test/test_helper.rb"
|
41
|
+
]
|
42
|
+
|
43
|
+
if s.respond_to? :specification_version then
|
44
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
45
|
+
s.specification_version = 3
|
46
|
+
|
47
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
48
|
+
s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
49
|
+
s.add_runtime_dependency(%q<mattetti-googlecharts>, [">= 0"])
|
50
|
+
else
|
51
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
52
|
+
s.add_dependency(%q<mattetti-googlecharts>, [">= 0"])
|
53
|
+
end
|
54
|
+
else
|
55
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
56
|
+
s.add_dependency(%q<mattetti-googlecharts>, [">= 0"])
|
57
|
+
end
|
58
|
+
end
|
data/lib/gchart_mod.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'gchart'
|
2
|
+
|
3
|
+
class Gchart
|
4
|
+
attr_accessor :grid_lines, :shape_markers
|
5
|
+
|
6
|
+
def set_shape_markers
|
7
|
+
shape_markers_values = case @shape_markers
|
8
|
+
when String
|
9
|
+
@shape_markers
|
10
|
+
when Array
|
11
|
+
if @shape_markers[0].is_a?(Array)
|
12
|
+
@shape_markers.map{|sm|sm.join(",")}.join("|")
|
13
|
+
else
|
14
|
+
@shape_markers.join("|")
|
15
|
+
end
|
16
|
+
when Hash
|
17
|
+
marker_type = @shape_markers[:marker_type]
|
18
|
+
color = @shape_markers[:color]
|
19
|
+
data_set_index = @shape_markers[:data_set_index]
|
20
|
+
data_point = @shape_markers[:data_point]
|
21
|
+
size = @shape_markers[:size]
|
22
|
+
priority = @shape_markers[:priority]
|
23
|
+
[marker_type,color,data_set_index,data_point,size,priority].join(",")
|
24
|
+
else
|
25
|
+
@shape_makers.to_s
|
26
|
+
end
|
27
|
+
"chm=#{shape_markers_values}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def set_grid_lines
|
31
|
+
grid_lines_values = case @grid_lines
|
32
|
+
when String
|
33
|
+
@grid_lines
|
34
|
+
when Array
|
35
|
+
@grid_lines.join(",")
|
36
|
+
when Hash
|
37
|
+
x_step = @grid_lines[:x_step]
|
38
|
+
y_step = @grid_lines[:y_step]
|
39
|
+
line_length = @grid_lines[:line_length]
|
40
|
+
blank_length = @grid_lines[:blank_length]
|
41
|
+
x_offset = @grid_lines[:x_offset]
|
42
|
+
y_offset = @grid_lines[:y_offset]
|
43
|
+
[x_step,y_step,line_length,blank_length,x_offset,y_offset].join(",")
|
44
|
+
else
|
45
|
+
@grid_lines.to_s
|
46
|
+
end
|
47
|
+
"chg=#{grid_lines_values}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def query_builder(options="")
|
51
|
+
dataset
|
52
|
+
query_params = instance_variables.map do |var|
|
53
|
+
case var
|
54
|
+
when '@data'
|
55
|
+
set_data unless @data == []
|
56
|
+
# Set the graph size
|
57
|
+
when '@width'
|
58
|
+
set_size unless @width.nil? || @height.nil?
|
59
|
+
when '@type'
|
60
|
+
set_type
|
61
|
+
when '@title'
|
62
|
+
set_title unless @title.nil?
|
63
|
+
when '@legend'
|
64
|
+
set_legend unless @legend.nil?
|
65
|
+
when '@bg_color'
|
66
|
+
set_colors
|
67
|
+
when '@chart_color'
|
68
|
+
set_colors if @bg_color.nil?
|
69
|
+
when '@bar_colors'
|
70
|
+
set_bar_colors
|
71
|
+
when '@bar_width_and_spacing'
|
72
|
+
set_bar_width_and_spacing
|
73
|
+
when '@axis_with_labels'
|
74
|
+
set_axis_with_labels
|
75
|
+
when '@axis_range'
|
76
|
+
set_axis_range if dataset
|
77
|
+
when '@axis_labels'
|
78
|
+
set_axis_labels
|
79
|
+
when '@range_markers'
|
80
|
+
set_range_markers
|
81
|
+
when '@geographical_area'
|
82
|
+
set_geographical_area
|
83
|
+
when '@country_codes'
|
84
|
+
set_country_codes
|
85
|
+
when '@grid_lines'
|
86
|
+
set_grid_lines
|
87
|
+
when '@shape_markers'
|
88
|
+
set_shape_markers
|
89
|
+
when '@custom'
|
90
|
+
@custom
|
91
|
+
end
|
92
|
+
end.compact
|
93
|
+
|
94
|
+
# Use ampersand as default delimiter
|
95
|
+
unless options == :html
|
96
|
+
delimiter = '&'
|
97
|
+
# Escape ampersand for html image tags
|
98
|
+
else
|
99
|
+
delimiter = '&'
|
100
|
+
end
|
101
|
+
|
102
|
+
jstize(@@url + query_params.join(delimiter))
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
data/lib/google_otg.rb
ADDED
@@ -0,0 +1,337 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__))
|
2
|
+
require 'gchart_mod'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module GoogleOtg
|
6
|
+
|
7
|
+
DEFAULT_RANGE = 30 # 30 min
|
8
|
+
|
9
|
+
def google_line_graph(hits, args = {})
|
10
|
+
|
11
|
+
raise ArgumentError, "Invalid hits" unless hits && hits.length > 0
|
12
|
+
|
13
|
+
size = args.has_key?(:size) ? args[:size] : '800x200'
|
14
|
+
title = args.has_key?(:title) ? args[:title] : "Graph"
|
15
|
+
title_color = args.has_key?(:title_color) ? args[:title_color] : '000000'
|
16
|
+
title_size = args.has_key?(:title_size) ? args[:title_size] : '20'
|
17
|
+
grid_lines = args.has_key?(:grid_lines) ? args[:grid_lines] : [25,50]
|
18
|
+
legend = args.has_key?(:legend) ? args[:legend] : nil
|
19
|
+
|
20
|
+
x_labels = []
|
21
|
+
y_labels = [0]
|
22
|
+
data = []
|
23
|
+
|
24
|
+
if hits[0].is_a?(Array)
|
25
|
+
shape_markers = [['o','0000ff',0,'-1.0',6],['o','FF6600',1,'-1.0',6]]
|
26
|
+
line_colors = ['6699CC','FF9933']
|
27
|
+
|
28
|
+
hits.map{|h|
|
29
|
+
converted = hits_to_gchart_range(h, args)
|
30
|
+
data.push(converted[:points])
|
31
|
+
x_labels = converted[:x_labels] if converted[:x_labels].length > x_labels.length
|
32
|
+
y_labels = converted[:y_labels] if converted[:y_labels].max > y_labels.max
|
33
|
+
}
|
34
|
+
|
35
|
+
else
|
36
|
+
shape_markers = ['o','0000ff',0,'-1.0',6]
|
37
|
+
line_colors = '6699CC'
|
38
|
+
|
39
|
+
converted = hits_to_gchart_range(hits, args)
|
40
|
+
data.push(converted[:points])
|
41
|
+
x_labels = converted[:x_labels]
|
42
|
+
y_labels = converted[:y_labels]
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
axis_with_labels = 'x,y'
|
47
|
+
axis_labels = [x_labels,y_labels]
|
48
|
+
|
49
|
+
return Gchart.line(
|
50
|
+
:size => size,
|
51
|
+
:title => title,
|
52
|
+
:title_color => title_color,
|
53
|
+
:title_size => title_size,
|
54
|
+
:grid_lines => grid_lines,
|
55
|
+
:shape_markers => shape_markers,
|
56
|
+
:data => data,
|
57
|
+
:axis_with_labels => axis_with_labels,
|
58
|
+
:legend => legend,
|
59
|
+
:axis_labels => axis_labels,
|
60
|
+
:line_colors => line_colors)
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
def over_time_graph(hits, args = {})
|
65
|
+
height = args.has_key?(:height) ? args[:height] : 125
|
66
|
+
src = args.has_key?(:src) ? args[:src] : "http://www.google.com/analytics/static/flash/OverTimeGraph.swf"
|
67
|
+
|
68
|
+
range = hits_to_otg_range(hits, args)
|
69
|
+
vars = range_to_flashvars(range)
|
70
|
+
|
71
|
+
html = <<-eos
|
72
|
+
<embed width="100%" height="#{height}"
|
73
|
+
wmode="opaque" salign="tl" scale="noScale" quality="high" bgcolor="#FFFFFF"
|
74
|
+
flashvars="input=#{vars}"
|
75
|
+
pluginspage="http://www.macromedia.com/go/getflashplayer" type="application/x-shockwave-flash"
|
76
|
+
src="#{src}"/>
|
77
|
+
eos
|
78
|
+
|
79
|
+
return html
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
def google_pie(hits, label_fn, args = {})
|
84
|
+
height = args.has_key?(:height) ? args[:height] : 125
|
85
|
+
width = args.has_key?(:width) ? args[:width] : 125
|
86
|
+
pie_values = extract_pct_values(hits, label_fn, args)
|
87
|
+
vars = pie_to_flashvars(pie_values, args)
|
88
|
+
src = args.has_key?(:src) ? args[:src] : "http://www.google.com/analytics/static/flash/pie.swf"
|
89
|
+
|
90
|
+
html = <<-eos
|
91
|
+
<embed
|
92
|
+
width="#{width}"
|
93
|
+
height="#{height}"
|
94
|
+
salign="tl"
|
95
|
+
scale="noScale"
|
96
|
+
quality="high"
|
97
|
+
bgcolor="#FFFFFF"
|
98
|
+
flashvars="input=#{vars}&locale=en-US"
|
99
|
+
pluginspage="http://www.macromedia.com/go/getflashplayer"
|
100
|
+
type="application/x-shockwave-flash"
|
101
|
+
src="#{src}"/>
|
102
|
+
eos
|
103
|
+
return html
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
def pie_to_flashvars(args = {})
|
108
|
+
|
109
|
+
labels = args[:labels]
|
110
|
+
raw_values = args[:raw_values]
|
111
|
+
percent_values = args[:percent_values]
|
112
|
+
|
113
|
+
options = {
|
114
|
+
:Pie => {
|
115
|
+
:Id => "Pie",
|
116
|
+
:Compare => false,
|
117
|
+
:HasOtherSlice => false,
|
118
|
+
:RawValues => raw_values,
|
119
|
+
:Format => "DASHBOARD",
|
120
|
+
:PercentValues => percent_values
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
return URI::encode(options.to_json)
|
125
|
+
|
126
|
+
end
|
127
|
+
protected :pie_to_flashvars
|
128
|
+
|
129
|
+
def extract_pct_values(hits, label_fn, args = {})
|
130
|
+
|
131
|
+
limit = args.has_key?(:limit) ? args[:limit] : 0.0
|
132
|
+
|
133
|
+
total = 0.0
|
134
|
+
other = 0.0
|
135
|
+
percent_values = []
|
136
|
+
raw_values = []
|
137
|
+
labels = []
|
138
|
+
values = []
|
139
|
+
hits.each{|hit|
|
140
|
+
total += hit.count.to_f
|
141
|
+
}
|
142
|
+
hits.each{|hit|
|
143
|
+
ct = hit.count.to_f
|
144
|
+
pct = (ct / total)
|
145
|
+
|
146
|
+
if pct > limit
|
147
|
+
percent_values.push([pct, sprintf("%.2f%%", pct * 100)])
|
148
|
+
raw_values.push([ct, ct])
|
149
|
+
|
150
|
+
label = label_fn.call(hit)
|
151
|
+
meta = args.has_key?(:meta) ? args[:meta].call(hit) : nil
|
152
|
+
|
153
|
+
labels.push(label)
|
154
|
+
values.push({:label => label, :meta => meta, :percent_value => [pct, sprintf("%.2f%%", pct * 100)], :raw_value => ct})
|
155
|
+
else
|
156
|
+
other += ct
|
157
|
+
end
|
158
|
+
}
|
159
|
+
if other > 0.0
|
160
|
+
pct = other / total
|
161
|
+
percent_values.push([pct, sprintf("%.2f%%", pct * 100)])
|
162
|
+
raw_values.push([other, other])
|
163
|
+
labels.push("Other")
|
164
|
+
values.push({:label => "Other", :percent_value => [pct, sprintf("%.2f%%", pct * 100)], :raw_value => other})
|
165
|
+
end
|
166
|
+
|
167
|
+
return {:labels => labels, :raw_values => raw_values, :percent_values => percent_values, :values => values}
|
168
|
+
|
169
|
+
end
|
170
|
+
protected :extract_pct_values
|
171
|
+
|
172
|
+
def flto10(val)
|
173
|
+
return ((val / 10) * 10).to_i
|
174
|
+
end
|
175
|
+
protected :flto10
|
176
|
+
|
177
|
+
def hits_to_otg_range(hits, args = {})
|
178
|
+
return hits_to_range(hits, lambda {|count, date_key, date_value|
|
179
|
+
{:Value => [count, count], :Label => [date_key, date_value]}
|
180
|
+
}, lambda{|mid, top|
|
181
|
+
[[mid,mid],[top,top]]
|
182
|
+
}, lambda{|hit, hit_date_key, hit_date_value|
|
183
|
+
[hit_date_key, hit_date_value]
|
184
|
+
},args)
|
185
|
+
end
|
186
|
+
|
187
|
+
def hits_to_gchart_range(hits, args = {})
|
188
|
+
return hits_to_range(hits, lambda {|count, date_key, date_value|
|
189
|
+
count
|
190
|
+
}, lambda {|mid, top|
|
191
|
+
[0,top/2,top]
|
192
|
+
},lambda{|hit, hit_date_key, hit_date_value|
|
193
|
+
hit_date_value
|
194
|
+
}, args)
|
195
|
+
end
|
196
|
+
|
197
|
+
def hits_to_range(hits, points_fn, y_label_fn, x_label_fn, args = {})
|
198
|
+
|
199
|
+
return nil unless hits
|
200
|
+
|
201
|
+
hits.map{|h|
|
202
|
+
if !h.respond_to?("created_at") || !h.respond_to?("count")
|
203
|
+
raise ArgumentError, "Invalid object type. All objects must respond to 'count' and 'created_at'"
|
204
|
+
end
|
205
|
+
}
|
206
|
+
|
207
|
+
tz = args.has_key?(:time_zone) ? args[:time_zone] : ActiveSupport::TimeZone['UTC']
|
208
|
+
Time.zone = tz
|
209
|
+
label = args.has_key?(:label) ? args[:label] : "Value"
|
210
|
+
time_fn = args.has_key?(:time_fn) ? args[:time_fn] : lambda {|h| h.created_at }
|
211
|
+
range = args.has_key?(:range) ? args[:range] : DEFAULT_RANGE
|
212
|
+
x_label_format = args.has_key?(:x_label_format) ? args[:x_label_format] : "%A %I:%M%p"
|
213
|
+
|
214
|
+
max_y = 0
|
215
|
+
hits_dict = {}
|
216
|
+
hits.each { |h|
|
217
|
+
hits_dict[time_fn.call(h)] = h
|
218
|
+
}
|
219
|
+
|
220
|
+
total = 0
|
221
|
+
|
222
|
+
points = []
|
223
|
+
point_dates = []
|
224
|
+
|
225
|
+
now_days = Time.now # use this get the right year, month and day
|
226
|
+
now_minutes = Time.at((Time.now.to_i/(60*range))*(60*range)).gmtime
|
227
|
+
now_floored = Time.local(now_days.year, now_days.month, now_days.day,
|
228
|
+
now_minutes.hour, now_minutes.min, now_minutes.sec)
|
229
|
+
|
230
|
+
current = hits.length > 0 ? time_fn.call(hits[0]) : now_floored
|
231
|
+
|
232
|
+
while (current < now_floored + range.minutes && range > 0) do
|
233
|
+
if hits_dict[current]
|
234
|
+
count = hits_dict[current].count.to_i
|
235
|
+
max_y = count if count > max_y
|
236
|
+
|
237
|
+
date = time_fn.call(hits_dict[current])
|
238
|
+
date_key = date.to_i
|
239
|
+
date_value = date.strftime(x_label_format)
|
240
|
+
|
241
|
+
points.push(points_fn.call(count, date_key, date_value))
|
242
|
+
total += count
|
243
|
+
else
|
244
|
+
|
245
|
+
date = current
|
246
|
+
date_key = date.to_i
|
247
|
+
date_value = date.strftime(x_label_format)
|
248
|
+
|
249
|
+
points.push(points_fn.call(0, date_key, date_value))
|
250
|
+
end
|
251
|
+
# Save the date for the x labels later
|
252
|
+
point_dates.push({:key => date_key, :value => date_value})
|
253
|
+
current = current + range.minutes
|
254
|
+
break if points.length > 100
|
255
|
+
end
|
256
|
+
|
257
|
+
## Setup Y axis labels ##
|
258
|
+
max_y = args.has_key?(:max_y) ? (args[:max_y] > max_y ? args[:max_y] : max_y) : max_y
|
259
|
+
|
260
|
+
top_y = self.flto10(max_y) + 10
|
261
|
+
mid_y = self.flto10(top_y / 2)
|
262
|
+
y_labels = y_label_fn.call(mid_y, top_y)
|
263
|
+
## end y axis labels ##
|
264
|
+
|
265
|
+
## Setup X axis labels
|
266
|
+
x_labels = []
|
267
|
+
max_x_label_count = args.has_key?(:max_x_label_count) ? args[:max_x_label_count] : points.length
|
268
|
+
|
269
|
+
if points.length > 0
|
270
|
+
step = points.length / max_x_label_count
|
271
|
+
idx = 0
|
272
|
+
|
273
|
+
while idx < points.length
|
274
|
+
point = points[idx]
|
275
|
+
date = point_dates[idx]
|
276
|
+
x_labels.push(x_label_fn.call(point, date[:key], date[:value]))
|
277
|
+
idx += step
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
## End x axis labels ##
|
282
|
+
|
283
|
+
return {:x_labels => x_labels, :y_labels => y_labels, :label => label, :points => points, :total => total}
|
284
|
+
|
285
|
+
end
|
286
|
+
protected :hits_to_range
|
287
|
+
|
288
|
+
def range_to_flashvars(args = {})
|
289
|
+
x_labels = args[:x_labels]
|
290
|
+
y_labels = args[:y_labels]
|
291
|
+
label = args[:label]
|
292
|
+
points = args[:points]
|
293
|
+
|
294
|
+
raise ArgumentError unless x_labels
|
295
|
+
raise ArgumentError unless y_labels
|
296
|
+
raise ArgumentError unless label
|
297
|
+
raise ArgumentError unless points
|
298
|
+
|
299
|
+
# this is the structure necessary to support the Google Analytics OTG
|
300
|
+
|
301
|
+
options = {:Graph => {
|
302
|
+
:Id => "Graph",
|
303
|
+
:ShowHover => true,
|
304
|
+
:Format => "NORMAL",
|
305
|
+
:XAxisTitle => "Day",
|
306
|
+
:Compare => false,
|
307
|
+
:XAxisLabels => x_labels,
|
308
|
+
:HoverType => "primary_compare",
|
309
|
+
:SelectedSeries => ["primary", "compare"],
|
310
|
+
:Series => [
|
311
|
+
{
|
312
|
+
:SelectionStartIndex => 0,
|
313
|
+
:SelectionEndIndex => points.length,
|
314
|
+
:Style =>
|
315
|
+
{
|
316
|
+
:PointShape => "CIRCLE",
|
317
|
+
:PointRadius => 9,
|
318
|
+
:FillColor => 30668,
|
319
|
+
:FillAlpha => 10,
|
320
|
+
:LineThickness => 4,
|
321
|
+
:ActiveColor => 30668,
|
322
|
+
:InactiveColor => 11654895
|
323
|
+
},
|
324
|
+
:Label => label,
|
325
|
+
:Id => "primary",
|
326
|
+
:YLabels => y_labels,
|
327
|
+
:ValueCategory => "visits",
|
328
|
+
:Points => points
|
329
|
+
}]
|
330
|
+
} # end graph
|
331
|
+
} # end options
|
332
|
+
|
333
|
+
return URI::encode(options.to_json)
|
334
|
+
end
|
335
|
+
protected :range_to_flashvars
|
336
|
+
|
337
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class GoogleOtgTest < ActiveSupport::TestCase
|
4
|
+
include GoogleOtg
|
5
|
+
|
6
|
+
context "GoogleOtg" do
|
7
|
+
context "when passed in bad data" do
|
8
|
+
should "throw ArgumentError" do
|
9
|
+
e = nil
|
10
|
+
begin
|
11
|
+
over_time_graph([:foo => "bar"])
|
12
|
+
rescue Exception => e
|
13
|
+
end
|
14
|
+
|
15
|
+
assert_instance_of(ArgumentError, e)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context "when passed in simple data" do
|
20
|
+
should "print out html" do
|
21
|
+
hits = Hit.find(:all, :order => "created_at" )
|
22
|
+
output = over_time_graph(hits)
|
23
|
+
md = output.strip!.match(/^\<embed/)
|
24
|
+
assert(md != nil)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "actionview is modified" do
|
29
|
+
should "have over_time_graph method" do
|
30
|
+
assert(ActionView::Base.instance_methods.include?("over_time_graph"))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "google chart" do
|
35
|
+
should "when passed in simple data" do
|
36
|
+
hits = Hit.find(:all, :order => "created_at" )
|
37
|
+
output = google_line_graph(hits)
|
38
|
+
md = output.match(/^http\:\/\/chart\.apis\.google\.com\/chart\?/)
|
39
|
+
assert(md != nil)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
class ApplicationHelperTest < ActionView::TestCase
|
47
|
+
def test_helper_method
|
48
|
+
#hits = Hit.find(:all, :order => "created_at" )
|
49
|
+
#tag = over_time_graph(hits)
|
50
|
+
#assert_tag_in(tag, :embed)#, :attributes => {:href => edit_account_path})
|
51
|
+
end
|
52
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
6
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
7
|
+
|
8
|
+
require 'active_record'
|
9
|
+
require 'action_controller'
|
10
|
+
require 'active_record/fixtures'
|
11
|
+
require 'active_support'
|
12
|
+
require 'action_pack'
|
13
|
+
require 'action_view'
|
14
|
+
require 'rails/init'
|
15
|
+
require 'action_view/test_case'
|
16
|
+
|
17
|
+
require 'google_otg'
|
18
|
+
|
19
|
+
class Test::Unit::TestCase
|
20
|
+
end
|
21
|
+
|
22
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
23
|
+
|
24
|
+
ActiveRecord::Schema.define(:version => 1) do
|
25
|
+
create_table :hits do |t|
|
26
|
+
t.integer :count
|
27
|
+
t.datetime :created_at
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Hit < ActiveRecord::Base
|
32
|
+
validates_presence_of :count
|
33
|
+
end
|
34
|
+
|
35
|
+
(1..3).each{|idx|
|
36
|
+
ca = (3 - idx).hours.ago
|
37
|
+
range = GoogleOtg::DEFAULT_RANGE
|
38
|
+
ca = Time.at((ca.to_i/(60*range))*(60*range))
|
39
|
+
|
40
|
+
Hit.create(:count => rand(65535), :created_at => ca)
|
41
|
+
}
|
42
|
+
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: google_otg
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.9
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- esilverberg
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-01 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: thoughtbot-shoulda
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: mattetti-googlecharts
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description: Include Google's Over Time Graph in your app
|
36
|
+
email: eric@ericsilverberg.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- LICENSE
|
43
|
+
- README.rdoc
|
44
|
+
files:
|
45
|
+
- .document
|
46
|
+
- .gitignore
|
47
|
+
- LICENSE
|
48
|
+
- README.rdoc
|
49
|
+
- Rakefile
|
50
|
+
- VERSION
|
51
|
+
- google_otg.gemspec
|
52
|
+
- lib/gchart_mod.rb
|
53
|
+
- lib/google_otg.rb
|
54
|
+
- rails/init.rb
|
55
|
+
- test/google_otg_test.rb
|
56
|
+
- test/test_helper.rb
|
57
|
+
has_rdoc: true
|
58
|
+
homepage: http://github.com/esilverberg/google_otg
|
59
|
+
licenses: []
|
60
|
+
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options:
|
63
|
+
- --charset=UTF-8
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
version:
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: "0"
|
77
|
+
version:
|
78
|
+
requirements: []
|
79
|
+
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 1.3.5
|
82
|
+
signing_key:
|
83
|
+
specification_version: 3
|
84
|
+
summary: Google's amazing over-time graph, in your rails app
|
85
|
+
test_files:
|
86
|
+
- test/google_otg_test.rb
|
87
|
+
- test/test_helper.rb
|