Google Charts on your site, the unobtrusive way
For a recent project, I needed to visualize some simple backend database statistics to admin users. Nothing exciting, just some simple numbers like count of subscriptions and purchases per day and such. When I started to work on some simple stats calculations, I already had in mind that I want to display the result in nice charts rather than in boring tables with numbers. And I wanted nice, interactive HTML 5 charts that display with almost no load time and fetch their data to display with Ajax.
There are several libraries that offer you to display charts in various ways like generating images to display or embedding Flash objects (yieks) into your page. Nowadays you surely want to use a library that displays interactive charts using Javascript and HTML5 though. In Railscast #223, there's a nice tutorial how to use a few libraries.
However, I ended up using Google's free chart service Google Charts, paired with unobtrusive Javascript using jQuery. Here's how I did it:
Btw, there are two kind of Google Charts (which was confusing initially). The Image Charts API can be used to display charts as plain images. You just need to build a URL with query parameters that specify what chart image you want and put it into an img
tag on your page. There are gems that provide helper methods to easily display image charts. However, the cool stuff comes with Google Chart Tools which adds nice, interactive charts using HTML5 to your page.
The way I used it:
1. The markup generated by the view contains an empty div
(rendered in almost no time) with a HTML5 data-attribute containing the URL where to fetch the chart data.
2. On the client side, jQuery pulls in the Google Chart library, fetches the chart data with an Ajax call and displays the chart.
3. On the server side a StatisticsController
handles these requests and returns chart data in JSON format.
A simple view helper
This is the statistics view helper (app/helpers/statistics_helper.rb
) used to output a simple div
tag in any view. Initially it displays an animated spinner image that displays right after the page loaded and indicates to the user that the chart ist loading. It also sets a height style attribute to size the chart area appropriately before it is loaded to preserve the page layout. The data-chart
attribute is set to the URL where the chart data can be GET.
module StatisticsHelper
def chart_tag (action, height, params = {})
params[:format] ||= :json
path = statistics_path(action, params)
content_tag(:div, :'data-chart' => path, :style => "height: #{height}px;") do
image_tag('spinner.gif', :size => '24x24', :class => 'spinner')
end
end
end
Example for usage in a view:
...
<%= chart_tag('subscriptions_graph', 300, :days => 14) %>
...
Unobtrusive Javascript on the client side
This little Javascript function (app/assets/javascripts/charts.js
) can be loaded on every page. It only loads the Google visualization library if there actually is a chart to display (an element with a data-chart attribute exists). It then fetches the chart data from the URL given in the data-chart
attribute and creates and displays the chart accordingly to the visualization API.
jQuery(function ($) {
// Load Google visualization library if a chart element exists
if ($('[data-chart]').length > 0) {
$.getScript('https://www.google.com/jsapi', function (data, textStatus) {
google.load('visualization', '1.0', { 'packages': ['corechart'], 'callback': function () {
// Google visualization library loaded
$('[data-chart]').each(function () {
var div = $(this)
// Fetch chart data
$.getJSON(div.data('chart'), function (data) {
// Create DataTable from received chart data
var table = new google.visualization.DataTable();
$.each(data.cols, function () { table.addColumn.apply(table, this); });
table.addRows(data.rows);
// Draw the chart
var chart = new google.visualization.ChartWrapper();
chart.setChartType(data.type);
chart.setDataTable(table);
chart.setOptions(data.options);
chart.setOption('width', div.width());
chart.setOption('height', div.height());
chart.draw(div.get(0));
});
});
}});
});
}
});
A controller that provides JSON chart data
The statistics controller (app/controllers/statistics_controller.rb
) provides one or more actions that return chart data in JSON format. You can control any aspect of the displayed chart from here. E.g. type
sets the type of the chart, cols
describe the data columns and rows
contains the actual data points. Refer to the visualization API (ChartWrapper) for a list of all properties.
class StatisticsController < ApplicationController
def subscriptions_graph
days = (params[:days] || 30).to_i
render :json => {
:type => 'AreaChart',
:cols => [['string', 'Date'], ['number', 'subscriptions'], ['number', 'purchases']],
:rows => (1..days).to_a.inject([]) do |memo, i|
date = i.days.ago.to_date
t0, t1 = date.beginning_of_day, date.end_of_day
subscriptions = Subscription.where(:created_at.gte => t0, :created_at.lte => t1).count
purchases = Purchase.where(:purchased_at.gte => t0, :purchased_at.lte => t1).count
memo << [date, subscriptions, purchases]
memo
end.reverse,
:options => {
:chartArea => { :width => '90%', :height => '75%' },
:hAxis => { :showTextEvery => 30 },
:legend => 'bottom',
}
}
end
end
Subscription
and Purchase
are my model classes. In case you wonder about the syntax, it's MongoID, not ActiveRecord.
Conclusion
Charts are now like I wanted them. The page loads almost instantly, showing a spinner to the user while the chart data is fetched (which can be a lengthy request). And those interactive HTML5 charts are nice eyecandy. The statistics controller may not scale well doing tons of count queries, but in this case it's used by a handful of admin users only. Also, more complex charts might need some adjustments in how the DataTable object is filled. Anyway, you get the point. ;)