Sunday, February 17, 2013

Integrating Flot Graphs in a Backbone View

Any data-driven application is going to need to perform a certain amount of transformations to the data in order to display it appropriately. Its generally more common in analytical areas where grouping and pivoting are required. As I've worked with Flot to build graphing solutions, I've needed to transform data from Backbone collections into something Flot could work with.

A Backbone collection's data is organized like this:

data = [
{ columnA: valueA, columnB: valueB, ... },
{ columnA: valueA, columnB: valueB, ... },
...
]


Where each index in data is a row with a hash of column/values.

While a Flot dataset is required to look like this:

data = [
{ label: seriesA, data: [ [x0,y0], [x1,y1] ... ] },
{ label: seriesB, data: [ [x0,y0], [x1,y1] ... ] },
{ label: seriesC, data: [ [x0,y0], [x1,y1] ... ] }
]


Where each index in data is a series with a hash describing the series. The data in the series object is an array of arrays describing each x/y point in the series.

As a more concrete example, consider the following Backbone collection data:

data = [
{ year: 2009, studio: 'Fox', gross: 760507625 },
{ year: 2010, studio: 'Par', gross: 658672302 },
{ year: 2010, studio: 'WB', gross: 623357910 },
{ year: 2009, studio: 'WB', gross: 534858444 },
{ year: 2011, studio: 'Fox', gross: 474544677 }
...
]


If we want to plot each gross by studio by year, we might setup our Flot data to look like this:

data = [
{ label: '2009', data: [ [0, 760507625], [1, 0], [2, 534858444], ... ] },
{ label: '2010', data: [ [0, 0], [1, 658672302], [2, 623357910], ... ] },
{ label: '2011', data: [ [0, 474544677], [1, 0], [2, 0], ... ] }
]

options.xaxis.ticks = [ [0, 'Fox'], [1, 'Par'], [2, 'WB'], ... ]


Each year becomes a different series in the graph and the points are the studios (x-axis) and gross dollars (y-axis). Note that we can't just pass the studio label as the x-axis value in the data array. Since those must be numbered indexes, the axis labels need to be setup in the options hash to describe each tick point. I lined those up to make it more clear what each element in the data array actually means.

Now that we have a basic visualization in mind, its time to build a Backbone View which will convert its collection into the required inputs for Flot to draw the graph. The view setup is pretty straight forward. I split the rendering up into two pieces. The main render() function just added the stub HTML required by Flot to hold the graph canvas. This needs to be styled properly to ensure it has a defined height/width. The graph is actually drawn in the renderGraph() function. This allows it to be called when something changes that requires the graph to be redrawn:


var MovieGraph = Backbone.View.extend({

attributes: { class: 'graph' },

seriesColumn: 'year',
xaxisColumn: 'studio',
yaxisColumn: 'gross',

plotOptions: {
yaxis: {},
xaxis: {},
legend: { show: true, container: '.legend' },
grid: { hoverable: true, clickable: true, autoHighlight: true },
series: {
stack: true,
bars: { show: true, fill: 0.7, barWidth: 0.8, align: 'center' }
}
},

render: function() {

this.$el.html('<div class="legend"></div><div class="plot"></div>');

return this;
},

renderGraph: function() {
/* See below */
}

});


Additionally, I setup my default options for the graph as well as identify which columns should be utilized in the graph dataset. The real work now happens in the drawGraph() function:


renderGraph: function() {

var data = this.collection.toJSON(),
options = _.clone(this.plotOptions),
sC = this.seriesColumn,
xC = this.xaxisColumn,
yC = this.yaxisColumn,
series = {},
xaxis = {},
base = {},
xticks = 0,
i;

if ( data.length > 0 ) {

// All x-axis data must be passed as an index
// from 0 .. n. Setup hash mapping of x-axis values to
// an indexed number so they can be looked up below
// Also, stacked charts don't work well with missing
// values so make sure each series will have a data point
// for each x-axis index.
for ( i=0;i<data.length;i++ ) {

if ( typeof xaxis[data[i][xC]] == 'undefined' ) {
xaxis[data[i][xC]] = xticks;
base[xticks] = 0;
xticks++;
}
}

// Build series data. Sum y-axis values over each series and x-axis point
for ( i=0;i<data.length;i++ ) {

if ( !series[data[i][sC]] ) {
// Initialize series with hash setup above to ensure
// each x-axis point has a value (zero). Set the label for
// the legend
series[data[i][sC]] = { data: _.clone( base ), label: data[i][sC] };
}

// Depending on the data, adding may be desired to group/summarize
series[data[i][sC]].data[ xaxis[data[i][xC]] ] += data[i][yC];
}

// Everything is a hash right now object[idx] = val
// Flot wants an array of arrays [ [idx, val] ... ]
// Use _.map to convert:
options.xaxis.ticks = _.map( xaxis, function ( val, idx ) { return [val, idx]; } );

// Same with the series data
for ( i in series )
series[i].data = _.map( series[i].data, function ( o, i ) { return [ i, o ]; });

// Again, another hash that must be an array of objects
// Convert again with _.map
series = _.map( series, function ( z ) { return z; } );

// Make the y-axis pretty. Assuming Gross dollars is the y-axis
// here. Format accordingly
options.yaxis.tickFormatter = function (val, axis) {
return Globalize.format(val, 'C0');
};

// Now, the chart can be drawn ...
$.plot( this.$('.plot'), series, options );
}

}


The basic process is to pivot the dataset according to the desired column layout and then convert everything into Flot speak before passing it to the plotting function. Its generally easier to work with object hashes when pivoting the data. However, that needs to be converted to arrays to be valid Flot input.

My sandbox has a working demo with full source. By creating variables in the view to define the series and x/y axis columns to use in the collection, you can see that this function is fairly reusable and can serve as the basis to build more complex graphing scenarios. The data is a bit sparse in this example but it should provide a good reference for how to apply it to larger datasets.