Building custom graphs with Raphaël

Recently for a current client project we needed to create an interactive graph. We wanted to build it using JavaScript and evaluated a number of really great opensource graphing libraries. The needs of this particular graph, however, were a bit more specialized than just plotting raw data, so we decided we needed to build our own graph. One of the graphing libraries we really liked is the awesome, though under-documented, gRaphaël. gRaphaël is based upon the Raphaël library, a JavaScript vector drawing library.

Brian McMurray, Software Architect
Posted

Recently for a current client project we needed to create an interactive graph. We wanted to build it using JavaScript and evaluated a number of really great opensource graphing libraries. The needs of this particular graph, however, were a bit more specialized than just plotting raw data, so we decided we needed to build our own graph. One of the graphing libraries we really liked is the awesome, though under-documented, gRaphaël. gRaphaël is based upon the Raphaël library, a JavaScript vector drawing library. Raphaël provides a simple unified API for drawing SVG and VML (for IE) graphics and adding interactivity.

It's fairly easy to get started with Raphaël and it integrates nicely with other libraries like jQuery. Similar to jQuery, Raphaël is extensible -- you can build your own "plug-ins" that can extend either the entire "canvas" to let you, for instance, create a graph builder, or you can extend individual drawing elements -- useful for wrapping up a common task (like a color transformation) into re-usable code.

Being able to extend Raphaël with new methods is great because you can wrap up all of your custom functionality and keep it contained. Furthermore, if you follow Douglas Crockford's excellent guidelines for creating public, private, and privaledged members in JavaScript your Raphaël extension can expose its own methods to allow further manipulation -- in the case of our client, we expose a method that allows for the data in our graph to be updated and the graph to be redrawn.

Taking Raphaël for a Test Drive with Drupal

The goal:
The empty graph displayed as a block.

The user graph showing the proportion of signed in users to total registered users.

Let's make a quick example graph using some simple information from Drupal. For our demo, we'll assume a bare-bones Drupal 6 site. We'll create a simple module that will provide a block with our graph in it. This graph will be a simple bar graph showing the number of users currently logged into the site. To highlight how we can use Crockford's priviledged member pattern, we'll also set up some simple AJAX polling so that this graph can update in real-time as users log in and out of the site.

The Basics

I won't go in-depth into most of the Drupal module files, but instead want to concentrate on the JavaScript we'll write, so let's get started:

usergraph.info :

  1. core = "6.x"
  2. project = "usergraph"
  3. name = "Users Graph"
  4. description = "Displays a simple bar graph of users signed in."

usergraph.module :

  1. <?php
  2.  
  3. /*
  4.  * Implementation of hook_block().
  5.  */
  6. function usergraph_block($op = 'list', $delta = 0, $edit = array()) {
  7. if ($op == 'list') {
  8. $blocks[0]['info'] = t('Users Signed-In Graph');
  9. return $blocks;
  10. }
  11. elseif ($op == 'view') {
  12. switch ($delta) {
  13. case 0:
  14. $block['content'] = usergraph_usergraph();
  15. $block['subject'] = t('Users Signed In');
  16. break;
  17. }
  18.  
  19. return $block;
  20. }
  21. }
  22.  
  23. /**
  24.  * Implementation of hook_theme().
  25.  */
  26. function usergraph_theme($existing, $type, $theme, $path) {
  27. return array(
  28. 'users_graph' => array(
  29. 'arguments' => array(),
  30. ),
  31. );
  32. }
  33.  
  34. function usergraph_usergraph() {
  35. // include the Raphael library
  36. drupal_add_js(drupal_get_path('module', 'usergraph') .'/js/raphael.packed.js');
  37.  
  38. // include our custom Raphael extension
  39. drupal_add_js(drupal_get_path('module', 'usergraph') . '/js/raphael.bar.js');
  40.  
  41. // include our Drupal behaviors
  42. drupal_add_js(drupal_get_path('module', 'usergraph') . '/js/usergraph.js');
  43.  
  44. $graph_data = usergraph_get_counts();
  45.  
  46. drupal_add_js($graph_data, 'setting');
  47.  
  48. // return the HTML necessary to attach the graph
  49. return theme('users_graph');
  50. }
  51.  
  52. function usergraph_get_counts() {
  53. // Adapted from user.module
  54. // Count users active within the defined period.
  55. $interval = time() - variable_get('user_block_seconds_online', 900);
  56. // Perform database queries to gather online user lists. We use s.timestamp
  57. // rather than u.access because it is much faster.
  58. $authenticated_users = db_query('SELECT DISTINCT u.uid, u.name, s.timestamp FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.timestamp >= %d AND s.uid > 0 ORDER BY s.timestamp DESC', $interval);
  59. $authenticated_count = 0;
  60. $items = array();
  61. while ($account = db_fetch_object($authenticated_users)) {
  62. $items[] = $account;
  63. $authenticated_count++;
  64. }
  65.  
  66. $total_users = db_fetch_object(db_query('SELECT COUNT(*) as count FROM {users} u WHERE u.status = 1'));
  67.  
  68. $graph_data = array(
  69. 'usergraph' => array(
  70. 'current' => $authenticated_count,
  71. 'total' => $total_users->count
  72. )
  73. );
  74.  
  75. return $graph_data;
  76. }
  77.  
  78. function theme_users_graph() {
  79. return "<div id='usergraph'><noscript>Enable JavaScript to see this awesome graph.</noscript></div>";
  80. }

So now we have a basic module set up that provides a single block. This block doesn't do a whole lot yet, besides figure out how many users are registered on the site, how many are logged in, include those numbers into Drupal.settings and include a few JavaScript files -- the Raphaël library and two custom files -- raphael.bar.js and usergraph.js -- which we haven't created yet. You might wonder why I separate these into two files. My thought here is that raphael.bar.js is going to contain just the code for our specific Raphaël plug-in -- code to build a simple bar graph given some data, while usergraph.js is going to contain the Drupal.behaviors JavaScript that will instantiate the bar graph and handle communicating with Drupal to provide data to the graph.

raphael.bar.js :

[js]/*
 * Provide a custom Raphaël extension to create a very simple horizontal bar graph.
 */
Raphael.fn.bar = Raphael.fn.bar || function(config) {
  var that = this, // so that we can refer to the main context from within other functions in scope.
      canvasWidth = config.width,
      canvasHeight = config.height;

  /*
   * A private method that can't be called from an external reference to the graph.
   * This method can only be called internally or via a public method calling it.
   * See this.setData();
   */
  function makeGraph(data, canvas) {
    var total = data.total,
        current = data.current,
        graphWidth = canvasWidth - 10;

    canvas.rect(0,5, graphWidth, 50).attr({fill: "black"});

    canvas.rect(0,5, ((parseInt(current)/parseInt(total)) * graphWidth) , 50)
          .attr({fill: "blue"});
  }

  /*
   * Protected method allows data to be sent to the chart and
   * call the private makeGraph method.
   */
  this.setData = function(data) {
    makeGraph(data, that);
  }

  return this;
}[/js]

In Raphaël, we create a new plug-in very similarly to creating a new jQuery plugin, by adding a new method to the Raphael.fn (pronounced "eff-en") namespace. In our case, we're adding a new method called 'bar'. Our method takes one parameter called config which will be a configuration object with various properties that allow us to customize this particular graph instance.

Following Douglas Crockford's patterns for private and protected methods, we create a private method called makeGraph<span class="br0">(</span><span class="br0">)</span> which receives 'data' and 'canvas' parameters. The 'data' parameter will be a simple Object with two properties -- total which is an Integer of the total number of users and current which is another Integer representing the number of registered users currently signed in. Given this information, we then draw two simple rectangles -- one to represent the full width of the graph, and another which highlights the proportion of currently signed-in users to the total.

Finally we create the this<span class="sy0">.</span>setData<span class="br0">(</span><span class="br0">)</span> method, a protected method which allows us to pass in data to this graph instance to create the graph.

Now that we have a new Raphaël plug-in, let's write some code to instantiate this and populate it with our user data.

usergraph.js :

[js]/*
 * Instantiate a bar graph in Raphaël and populate it with data
 * from Drupal.settings.usergraph
 */
Drupal.behaviors.usergraph = function(context) {
  var r, chart,
      chartHeight = 250,
      chartWidth = 200;

  if ($('#usergraph:not(.graph-processed)', context).length) {
    r = Raphael($('#usergraph:not(.graph-processed)', context).addClass('graph-processed').get(0), chartWidth, chartHeight);

    chart = r.bar({
      /* we have to pass in height and width because IE doesn't read it
         correctly from raphael */
      width: chartWidth,
      height: chartHeight
    });

    if (Drupal.settings.usergraph) {
      chart.setData(Drupal.settings.usergraph);
    }
  }
}[/js]

In usergraph.js we're extending the Drupal.behaviors namespace with a method to create our user graph. We set up some size constraints, use jQuery to find a div with an id of "usergraph" that hasn't yet been processed by this behavior and instantiate Raphaël on it. Then, we create an instance of our new graph within Raphaël with chart <span class="sy0">=</span> r<span class="sy0">.</span>bar<span class="br0">(</span><span class="sy0">...</span><span class="br0">)</span><span class="sy0">;</span>.

At this point, we won't see anything yet because we haven't given our graph any data. So next we check to see if Drupal.settings.usergraph exists, and, if so, call the setData() method on our chart passing in the usergraph settings.

And just like that we now have a simple chart that shows us the proportion of registered users who are currently signed in. (Be sure to assign your block to a region so you can actually see the block in action!)

Making Our Graph Shine with AJAX

This graph, however, is still static. It's built once when the page loads and then never updates. Let's take a look now at how we can quickly add some simple AJAX polling to update this graph in near real-time.

We'll need to add a little bit more to usergraph.module to add a menu callback that can provide an up-to-date JSON response of how many registered and signed in users there are, and then modify usergraph.js to set up a timed process to poll for updated data and re-draw our graph.

Let's add to usergraph.module :

[js]/*
 * Implementation of hook_menu().
 */
function usergraph_menu() {
  $items = array();

  // Homepage
  $items['ajax/usergraph'] = array(
    'title' => '',
    'page callback' => 'usergraph_ajax_update',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );

  return $items;
}

function usergraph_ajax_update() {
  $graph_data = usergraph_get_counts();

  drupal_json($graph_data);
  exit;
}[/js]

And now we'll modify usergraph.js :

[js]/*
 * Instantiate a bar graph in Raphaël and populate it with data
 * from Drupal.settings.usergraph
 */
Drupal.behaviors.usergraph = function(context) {
  var r, chart,
      chartHeight = 250,
      chartWidth = 200,
      graphTimer,
      updateInterval = 5000;

  if ($('#usergraph:not(.graph-processed)', context).length) {
    r = Raphael($('#usergraph:not(.graph-processed)', context).addClass('graph-processed').get(0), chartWidth, chartHeight);

    chart = r.bar({
      /* we have to pass in height and width because IE doesn't read it
         correctly from raphael */
      width: chartWidth,
      height: chartHeight
    });

    if (Drupal.settings.usergraph) {
      chart.setData(Drupal.settings.usergraph);
    }

    function updateGraph() {
      window.clearTimeout(graphTimer);

      // make an AJAX request to fetch update stats
      $.ajax({
        url: Drupal.settings.basePath + 'ajax/usergraph',
        success: function(data, status, xhr) {
          // parse the response as JSON
          var response = JSON.parse(data);

          // update the graph
          chart.setData(response.usergraph);

          // reset our timer
          graphTimer = window.setTimeout(updateGraph, updateInterval);
        }
      });
    }

    // set up a timer to start polling for updates
    window.clearTimeout(graphTimer);
    graphTimer = window.setTimeout(updateGraph, updateInterval);

  }
}[/js]

So in these changes we add a simple AJAX page callback in our module which provides a simple JSON representation of what we'd usually find in Drupal.settings.usergraph. Then, in our JavaScript file, we create a timer with a default timing of 5 seconds, which will perform a simple jQuery AJAX request for our data. If the AJAX request returns successfully, we parse the JSON response, then send the data to our graph to update it and restart the timer so the polling will start again.

The JSON returned by our AJAX endpoint.

And there we have it, a simple, custom Raphaël plug-in that is fed near real-time data from Drupal and turns it into a handy horizontal bar chart.

Have you been experimenting with Raphaël? Let us know in the comments what you've been trying!

The source for this simple demo is available here: https://bitbucket.org/treehouseagency/usergraph but also check out the Raphaël module for Drupal which uses the gRaphaël library to create neat graphs similar to this one. Thanks!

Brian McMurray

Software Architect