Facebook usage by year

We will look at a simple timeline visualization that allows the user to pan across a graph representing the total number of users registered on Facebook from 2004 to 2012.

You can View the demo here.

Getting started

Our markup for this visualization contains a brief message informing the user that they can pan the graph left and right by clicking left or right on the canvas.
<h1>Facebook Usage by Year</h1>
<p>Clicking to the right or left of the canvas pans...</p>
<div id="facebook-usage"></div>
<script type="text/javascript" src="raphael-2.1.0-min.js"></script>
<script type="text/javascript" src="jquery-1.9.1.min.js"></script>
<script type="text/javascript" src="facebook-usage.js"></script>

We write our JavaScript in facebook-usage.js where we define a 770px by 450px canvas as follows:

var paper = Raphael('facebook-usage', 770, 450);

Facebook usage data by year

The data for this visualization comes courtesy of Yahoo! Finance at Yahoo! Finance. Each data usage point represents the total number of users of Facebook at the year end as given.

-----------------------------------------
Year    Total number of users at year end
-----------------------------------------
2004    1,000,000
2005    5,500,000
2006    12,000,000
2007    50,000,000
2008    150,000,000
2009    350,000,000
2010    608,000,000
2011    845,000,000
2012    1,000,000,000
-----------------------------------------

We define an array of objects of the form shown:

var data = [
    {
        year: '2004',
        users: 1000000,
        text: 'Facebook was born in 2004 and by the end ...'
    },
    // ...
];

Point co-ordinates and default values

We will be drawing a graph onto our canvas meaning that we need to consider the overall layout of our drawing region and define a number of default values that will determine where points are plotted. Gutter values (white space regions at the edges of our canvas are defined wherein no point can be drawn), point radii and the distance of descriptive text from a point are defined:

var gutter_bottom = 100,
    point_radius = 5,
    gutter_top = 100,
    text_from_point = 50,
    gutter_left = 40,
    min_y_point = 1000000,
    max_y_point = 1100000000,
    num_data_points = data.length;

The getY and getX functions

In order that point plotting takes into consideration the default values defined above (for example: does not plot points in gutters) we define helper functions to return x and y values relevant to our canvas.

function getY(point) {
    // (a) determine height available minus gutters
    var usable_height = paper.height - gutter_top 
        - gutter_bottom; 
    // (b) determine point relative to available height
    var y = usable_height * 
        (point - min_y_point) / (max_y_point - min_y_point);
    // (c) reverse the y point to be drawn from the bottom
    var y = usable_height - y;

    return gutter_top + y;
}

The getY function returns a y point that (a) falls in between the regions defined by the top and bottom gutters, (b) is based on the maximum and minimum x and y data values, being a fraction of the difference between these two points and (c) is drawn from the bottom of our canvas. In the case of (c), the expression usable_height - y has the effect of drawing our point as if the origin point were at the bottom left of our canvas (minus gutters). The equation in (b) effectively normalizes our data to say that the minimum data point is at the y = 0 point while our maximum data point is at the maximum y point for our drawing.

Our getX function simply ensures that the value given is equal to the spacing defined between two points and is offset by the left gutter. Note that the x-spacing between two points is defined as 350px. Since we have more than three data points, they will be drawn outside of the rightmost point of our canvas - this is our aim so that we can pan the canvas later.

function getX(index) {
    if(num_data_points <= 1) {
        return 0;
    }
    var spacing = getXSpacing(index);
    return gutter_left + (spacing * index);
}

function getXSpacing(index) {
    return 350;
} 

Note that getX takes an index as its argument which corresponds to index of the data point in our data array that we are plotting. getY accepts as a value the actual data value defined in our data array.

Plotting data points

To plot our data points, we iterate over our data array and draw a circle at each x and y point defined in our data array. During each iteration of the loop, we build a Catmull-Rom spline in the manner detailed in the chapter 3, Drawing Paths. Given that each element of our data array has associated text, we plot this at a point relative to each data point’s (x, y) co-ordinates.

var path = ['M'];
for(var i = 0; i < num_data_points; i+=1) {
    var x = getX(i);
    var y = getY(data[i].users);

    // Plot point as circle
    paper.circle(x, y, point_radius).attr({
        'stroke-width': 0,
        fill: '#3B5998'
    });

    // Add to Catmull-Rom spline path
    path.push(x);
    path.push(y);
    if(i === 0) {
        path.push('R');
    }

    // The year text for this point
    var txt = paper.text(x, 
        paper.height - gutter_bottom + 20, 
        data[i].year
    ).attr({
        'font-size': 11
    });

    // The description text next to each point
    var description = paper.text(x, 
        y - text_from_point, 
        data[i].text
    ).attr({
        'font-size': 11,
        'text-anchor': 'start'
    });
}

Each data point circle has a radius defined by the default point_radius variable and zero stroke width. Note that the year associated with each point is plotted as text in the bottom gutter region defined by the height of the paper minus the bottom gutter. The description text is plotted at the point y minus the default pixel value of text from a point. This places a nice description above each data point.

Finally we plot our Catmull-Rom curve based on the path we have just built.
var curve = paper.path(path).attr({
    'stroke-width': 1,
    'stroke-dasharray': '- ',
    stroke: '#bbb'
});
curve.toBack();

Note that we call toBack on our curve to ensure it sits behind our data point circles.

Panning the graph

Having drawn our graph, we need to be able to pan left and right. We do this when our canvas is clicked, panning left if the click point is to the left of the imaginary line drawn in y at the mid x-point on our canvas and panning right if the click point is to the right.

We register click event handlers on our canvas by getting the canvas attribute - which is a DOM element - of our paper object.

var xOffset = 0;
$(paper.canvas).click(function(e) {
   // Event handling code here...
});

Note that we have defined our initial x-offset as a variable outside the scope of our click event handler. This allows us track the current x-pan of our graph.

The event handling function is passed the event object, e, which allows us to get our current x and y mouse position relative to the canvas as follows:

var mouseX = e.pageX - this.offsetLeft,
    mouseY = e.pageY - this.offsetTop;

The center x point is determined as half of the width of the canvas (which is referenced by the this keyword) and we get our default xSpacing which will be how much we need to pan our graph by. The mouse offset determines whether we need to pan in the negative x or y direction based on whether the click point is to the left or right of the center x point (mouseX - cx < 0 indicates that it is to the left while a mouseX - cx >= 0 indicates that it is to the right).

var cx = $(this).width() / 2;
var xSpacing = getXSpacing();
var mouse_offset = (mouseX - cx) < 0 ? -xSpacing : xSpacing;

Finally, we do our panning, providing of course that our x offset has not already extended the bounds of our graph:

if((xOffset + mouse_offset) >= 0 && 
    (xOffset + mouse_offset) <= ( (num_data_points - 1) * xSpacing )
) {
    xOffset += mouse_offset;
    paper.setViewBox(xOffset, 0, paper.width, paper.height);
}

The (xOffset + mouse_offset) checks ensure that our proposed panning is :

Where these conditions hold true we increment the x offset by the proposed mouse offset and then set the view box on our canvas.

The setViewBox method has the effect of changing the boundaries of our canvas. In this instance, we are using it change the x point of the top left corner of our canvas to our newly defined xOffset.

Summary

This example gives us plenty of scope for drawing large drawings that don’t fit nicely within the given viewport region. Where data grows over time, this particular example works very well - as we pan through the graph we witness an ever-increasing number of user registrations at Facebook.