

First off, if you haven’t already, take a look at the Gruff RDoc – there is a lot of features hidden under the covers! Assuming you have the gem and the plugin installed, let’s get right to it. First the code, then the explanations:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def stats | |
g = Gruff::Line.new('580x210') | |
g.theme = { | |
:colors => ['#ff6600', '#3bb000', '#1e90ff', '#efba00', '#0aaafd'], | |
:marker_color => '#aaa', | |
:background_colors => ['#eaeaea', '#fff'] | |
} | |
g.hide_title = true | |
g.font = File.expand_path('path/to/font.ttf', RAILS_ROOT) | |
range = "created_at #{(12.months.ago.to_date..Date.today).to_s(:db)}" | |
@users = User.count(:all, :conditions => range, :group => "DATE_FORMAT(created_at, '%Y-%m')", :order =>"created_at ASC") | |
@votes = Vote.count(:all, :conditions => range, :group => "DATE_FORMAT(created_at, '%Y-%m')", :order =>"created_at ASC") | |
@bookmarks = Bookmark.count(:all, :conditions => range, :group => "DATE_FORMAT(created_at, '%Y-%m')", :order =>"created_at ASC") | |
# Take the union of all keys & convert into a hash {1 => "month", 2 => "month2"...} | |
# - This will be the x-axis.. representing the date range | |
months = (@users.keys | @votes.keys | @bookmarks.keys).sort | |
keys = Hash[*months.collect {|v| [months.index(v),v.to_s] }.flatten] | |
# Plot the data - insert 0's for missing keys | |
g.data("Users", keys.collect {|k,v| @users[v].nil? ? 0 : @users[v]}) | |
g.data("Votes", keys.collect {|k,v| @votes[v].nil? ? 0 : @votes[v]}) | |
g.data("Bookmarks", keys.collect {|k,v| @bookmarks[v].nil? ? 0 : @bookmarks[v]}) | |
g.labels = keys | |
send_data(g.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "site-stats.png") | |
end |
First few lines should be self-explanatory, we specify custom geometry, our theme colors, hide the title, and finally indicate the font we would like to use in the image. (Note: If RMagick complains about being unable to measure the font-sizes, make sure you have ‘freetype-fonts’ installed on your system) Next, is the SQL aggregation code, here we want to count the number of new users, votes and bookmarks on monthly basis:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
range = "created_at #{(12.months.ago.to_date..Date.today).to_s(:db)}" | |
# .to_s(:db) is a helper to transform a date-range into SQL condition clause | |
# ex. above: => "created_at BETWEEN '2006-01-05' AND '2007-01-05'" | |
@users = User.count(:all, :conditions => range, :group => "DATE_FORMAT(created_at, '%Y-%m')", :order =>"created_at ASC") | |
# - count all users, whose created_at timestamp falls within the range specified above | |
# - group users by 'Year-Month', and count the number of occurences within each group | |
# ex. result: | |
# count | date | |
# --------------- | |
# 2 | 2005-10 | |
# 24 | 2005-11 | |
# 32 | 2005-12 |
There is one catch, if we have no new records in one of the intervals, than the count will be missing from the returned OrderedHash – we need to guard against this by injecting ‘0′ counts for the missing intervals:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# first, we will take the union of the date arrays and sort our results | |
# ex: [1,2] | [2,3] => [1, 2, 3] | |
months = (@users.keys | @votes.keys | @bookmarks.keys).sort | |
# This is a tricky one. For the x-axis labels, Gruff requires a hash in the form of: | |
# { 1 => "label", 2=> "label2" ... } | |
# Hence, we have a 'months' array, which we need to convert it into a hash. | |
# 1) * is the splat operator - it will split our array and pass each value separately to the block | |
# - give it a try, in console do: p *[1,2,3] | |
# 2) Next, we will iterate over every value returned by the * operator and | |
# replace it with a [index, value] pair | |
# - ex: [a,b,c] => [[1,a],[2,b],[3,c]] | |
# 3) Now we flatten the resulting array and obtain: | |
# [[1,a],[2,b],[3,c]].flatten => [1,a,2,b,3,c] | |
# 4) Result is passed to the hash constructor, which converts every tuple | |
# into a key => value pair. Giving us: | |
# Hash[[1,a,2,b,3,c]] => { 1 => 'a', 2 => 'b', 3 => 'c' } | |
# | |
# Phew.. | |
keys = Hash[*months.collect {|v| [months.index(v),v.to_s] }.flatten] | |
# Now we can iterate over all keys ans insert the user values for the months | |
# that have a count, and otherwise provide a '0'. | |
g.data("Users", keys.collect {|k,v| @users[v].nil? ? 0 : @users[v]}) |
Almost done. After assigning the x-axis labels we call send_data to stream the resulting PNG image directly to the browser. Now all we have to do is embed our image into a page, or call it directly:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<p align="center"> | |
<img src="<%= url_for :controller => "admin", :action=> "stats" %>" style="border:1px solid #aabcca;" /> | |
</p> |
After adding a quick CSS border and centering the image, you can feast your eyes on the latest trends of your paradigm-shifting application. For performance reasons, I would also recommend caching the action if you plan to display your results in a high-traffic area. For my personal use, I am only showing the trends in a private administration section (shown on the left) – I can afford to regenerate the image on every refresh. For more ideas and Ruby graphing tutorials take a look at: Visual Database Explorer and Dynamic Graphics in Rails 2.2.

No comments:
Post a Comment