Dynamic Stat Graphs in Rails

What started out as a 'quick feature' turned out to be a fun Ruby endeavor - below is a guide with some neat Ruby tricks to create dynamic stat graphs (sample above) with the help of Gruff library. In my visual database explorer article I talked about generating SVG graphs with the help of Scruffy. However, SVG is a nightmare when it comes to browser compatibility issues and inline support, hence in this instance we will settle for RMagick on the backend.
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:
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:
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:
# 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:
<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 1.2.
About this entry
- Published:
- 05.01.07 / 10pm
- Category:
- Ruby on Rails
- Print:
-
PDF & Email friendly
Related Posts
- 07.03 Client HTTP Caching in Rails
- 11.12 Visual Database Explorer in Ruby
- 31.07 Reconstructing Request URIs in Rails
- 14.04 Automatic CS Paper Generator
- 17.09 Six Degrees - Duncan J. Watts









Entries RSS
20 Comments
comments rss | trackback uri