zargony.com

#![desc = "Random thoughts of a software engineer"]

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. ;)