$:.unshift(File.dirname(__FILE__))
require 'gchart_mod'
require 'uri'
module GoogleOtg
DEFAULT_RANGE = 30 # 30 min
def google_line_graph(hits, args = {})
raise ArgumentError, "Invalid hits" unless hits && hits.length > 0
size = args.has_key?(:size) ? args[:size] : '800x200'
title = args.has_key?(:title) ? args[:title] : "Graph"
title_color = args.has_key?(:title_color) ? args[:title_color] : '000000'
title_size = args.has_key?(:title_size) ? args[:title_size] : '20'
grid_lines = args.has_key?(:grid_lines) ? args[:grid_lines] : [25,50]
legend = args.has_key?(:legend) ? args[:legend] : nil
x_labels = []
y_labels = [0]
data = []
if hits[0].is_a?(Array)
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]]
line_colors = ['6699CC','FF9933']
lower_bound_time = nil
hits.map{|series| lower_bound_time = series[0] if !lower_bound_time || series[0].created_at < lower_bound_time.created_at}
args[:lower_bound_time] = lower_bound_time if lower_bound_time
hits.map{|h|
converted = hits_to_gchart_range(h, args)
data.push(converted[:points])
x_labels = converted[:x_labels] if converted[:x_labels].length > x_labels.length
y_labels = converted[:y_labels] if converted[:y_labels].max > y_labels.max
}
else
shape_markers = [['D','6699CC',0,'-1.0',4],['o','0000ff',0,'-1.0',8]]
line_colors = ['6699CC']
converted = hits_to_gchart_range(hits, args)
data.push(converted[:points])
x_labels = converted[:x_labels]
y_labels = converted[:y_labels]
end
axis_with_labels = 'x,y'
axis_labels = [x_labels,y_labels]
return Gchart.line(
:size => size,
:title => title,
:title_color => title_color,
:title_size => title_size,
:grid_lines => grid_lines,
:shape_markers => shape_markers,
:data => data,
:axis_with_labels => axis_with_labels,
:max_value => y_labels[y_labels.length - 1],
:legend => legend,
:axis_labels => axis_labels,
:line_colors => line_colors)
end
def over_time_graph(hits, args = {})
height = args.has_key?(:height) ? args[:height] : 125
src = args.has_key?(:src) ? args[:src] : "http://www.google.com/analytics/static/flash/OverTimeGraph.swf"
if hits.is_a?(Array) and hits[0].is_a?(Array)
lower_bound_time = nil
hits.map{|series| lower_bound_time = series[0] if !lower_bound_time || series[0].created_at < lower_bound_time.created_at}
args[:lower_bound_time] = lower_bound_time if lower_bound_time
range = hits.map{|h| hits_to_otg_range(h, args) }
else
range = [hits_to_otg_range(hits, args)]
end
vars = range_to_flashvars(range)
html = <<-eos
eos
return html
end
def google_pie(hits, label_fn, args = {})
height = args.has_key?(:height) ? args[:height] : 125
width = args.has_key?(:width) ? args[:width] : 125
pie_values = extract_pct_values(hits, label_fn, args)
vars = pie_to_flashvars(pie_values, args)
src = args.has_key?(:src) ? args[:src] : "http://www.google.com/analytics/static/flash/pie.swf"
html = <<-eos
eos
return html
end
def pie_to_flashvars(args = {})
labels = args[:labels]
raw_values = args[:raw_values]
percent_values = args[:percent_values]
options = {
:Pie => {
:Id => "Pie",
:Compare => false,
:HasOtherSlice => false,
:RawValues => raw_values,
:Format => "DASHBOARD",
:PercentValues => percent_values
}
}
return URI::encode(options.to_json)
end
protected :pie_to_flashvars
def extract_pct_values(hits, label_fn, args = {})
limit = args.has_key?(:limit) ? args[:limit] : 0.0
total = 0.0
other = 0.0
percent_values = []
raw_values = []
labels = []
values = []
hits.each{|hit|
total += hit.count.to_f
}
hits.each{|hit|
ct = hit.count.to_f
pct = (ct / total)
if pct > limit
percent_values.push([pct, sprintf("%.2f%%", pct * 100)])
raw_values.push([ct, ct])
label = label_fn.call(hit)
meta = args.has_key?(:meta) ? args[:meta].call(hit) : nil
labels.push(label)
values.push({:label => label, :meta => meta, :percent_value => [pct, sprintf("%.2f%%", pct * 100)], :raw_value => ct})
else
other += ct
end
}
if other > 0.0
pct = other / total
percent_values.push([pct, sprintf("%.2f%%", pct * 100)])
raw_values.push([other, other])
labels.push("Other")
values.push({:label => "Other", :percent_value => [pct, sprintf("%.2f%%", pct * 100)], :raw_value => other})
end
return {:labels => labels, :raw_values => raw_values, :percent_values => percent_values, :values => values}
end
protected :extract_pct_values
def flto10(val)
return ((val / 10) * 10).to_i
end
protected :flto10
def hits_to_otg_range(hits, args = {})
return hits_to_range(hits, lambda {|count, date_key, date_value|
{:Value => [count, count], :Label => [date_key, date_value]}
}, lambda{|mid, top|
[[mid,mid],[top,top]]
}, lambda{|hit, hit_date_key, hit_date_value|
[hit_date_key, hit_date_value]
},args)
end
def hits_to_gchart_range(hits, args = {})
return hits_to_range(hits, lambda {|count, date_key, date_value|
count
}, lambda {|mid, top|
[0,top/2,top]
},lambda{|hit, hit_date_key, hit_date_value|
hit_date_value
}, args)
end
def hits_to_range(hits, points_fn, y_label_fn, x_label_fn, args = {})
return nil unless hits
hits.map{|h|
if !h.respond_to?("created_at") || !h.respond_to?("count")
raise ArgumentError, "Invalid object type. All objects must respond to 'count' and 'created_at'"
end
}
tz = args.has_key?(:time_zone) ? args[:time_zone] : ActiveSupport::TimeZone['UTC']
label = args.has_key?(:label) ? args[:label] : "Value"
time_fn = args.has_key?(:time_fn) ? args[:time_fn] : lambda {|h|
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
}
range = args.has_key?(:range) ? args[:range] : DEFAULT_RANGE
x_label_format = args.has_key?(:x_label_format) ? args[:x_label_format] : "%A %I:%M%p"
max_y = 0
hits_dict = {}
hits.each { |h|
hits_dict[time_fn.call(h)] = h
}
total = 0
points = []
point_dates = []
now_days = tz.now # use this get the right year, month and day
now_minutes = tz.at((now_days.to_i/(60*range))*(60*range)).gmtime
now_floored = tz.local(now_days.year, now_days.month, now_days.day,
now_minutes.hour, now_minutes.min, now_minutes.sec)
if args[:lower_bound_time]
current = time_fn.call(args[:lower_bound_time])
else
current = hits.length > 0 ? time_fn.call(hits[0]) : now_floored
end
while (current < now_floored + range.minutes && range > 0) do
if hits_dict[current]
count = hits_dict[current].count.to_i
max_y = count if count > max_y
date = time_fn.call(hits_dict[current])
date_key = date.to_i
date_value = date.strftime(x_label_format)
points.push(points_fn.call(count, date_key, date_value))
total += count
else
date = current
date_key = date.to_i
date_value = date.strftime(x_label_format)
points.push(points_fn.call(0, date_key, date_value))
end
# Save the date for the x labels later
point_dates.push({:key => date_key, :value => date_value})
current = current + range.minutes
break if points.length > 100
end
## Setup Y axis labels ##
max_y = args.has_key?(:max_y) ? (args[:max_y] > max_y ? args[:max_y] : max_y) : max_y
top_y = self.flto10(max_y) + 10
mid_y = self.flto10(top_y / 2)
y_labels = y_label_fn.call(mid_y, top_y)
## end y axis labels ##
## Setup X axis labels
x_labels = []
max_x_label_count = args.has_key?(:max_x_label_count) ? args[:max_x_label_count] : points.length
if points.length > 0
step = [points.length / max_x_label_count, 1].max
idx = 0
while idx < points.length
point = points[idx]
date = point_dates[idx]
x_labels.push(x_label_fn.call(point, date[:key], date[:value]))
idx += step
end
end
## End x axis labels ##
return {:x_labels => x_labels, :y_labels => y_labels, :label => label, :points => points, :total => total}
end
protected :hits_to_range
PRIMARY_STYLE = {
:PointShape => "CIRCLE",
:PointRadius => 9,
:FillColor => 30668,
:FillAlpha => 10,
:LineThickness => 4,
:ActiveColor => 30668,
:InactiveColor => 11654895
}
COMPARE_STYLE = {
:PointShape => "CIRCLE",
:PointRadius => 6,
:FillAlpha => 10,
:LineThickness => 2,
:ActiveColor => 16750848,
:InactiveColor => 16750848
}
def setup_series(args, style)
raise ArgumentError unless args[:label]
raise ArgumentError unless args[:points]
raise ArgumentError unless args[:y_labels]
return {
:SelectionStartIndex => 0,
:SelectionEndIndex => args[:points].length,
:Style => style,
:Label => args[:label],
:Id => "primary",
:YLabels => args[:y_labels],
:ValueCategory => "visits",
:Points => args[:points]
} # end graph
end
protected :setup_series
def range_to_flashvars(args = {})
raise ArgumentError unless args.length > 0
x_labels = args[0][:x_labels]
raise ArgumentError unless x_labels
ct = 0
# this is the structure necessary to support the Google Analytics OTG
options = {:Graph => {
:Id => "Graph",
:ShowHover => true,
:Format => "NORMAL",
:XAxisTitle => "Day",
:Compare => false,
:XAxisLabels => x_labels,
:HoverType => "primary_compare",
:SelectedSeries => ["primary", "compare"],
:Series => args.map {|arg|
ct += 1
setup_series(arg, ct == 1 ? PRIMARY_STYLE : COMPARE_STYLE)
}
} # end graph
} # end options
return URI::encode(options.to_json)
end
protected :range_to_flashvars
end