December 16, 2011

Displaying Service-Based Data, Part 3

In my previous post I wrote about normalizing service-based data to a common format. Now I'm going to talk about getting that data onto the screen.

Continuing with our earlier example, say we've retrieved some weather data and transformed it into the following format:

{
  response: {
    meta: { ... some stuff ... },
    results: [
      { id:'KSFO', temp_f:45, wdir:170, wspd:4, pressure:1026, trend:'+', sky:'partly cloudy' },
      { id:'KHAF', temp_f:52, wdir:0, wspd:0, pressure:1025, trend:'+', sky:'clear' },
      { id:'KSQL', temp_f:45, wdir:0, wspd:0, pressure:1025, trend:'+', sky:'partly cloudy' },
      { id:'KOAK', temp_f:45, wdir:60, wspd:6, pressure:1026, trend:'+', sky:'partly cloudy' }
    ]
  }
}

Displaying this data consists of two steps:

  1. Getting the data into the right order and length
  2. Rendering the data

In some cases the data is already in the order we need because the API we used to retrieve it allowed us to specify sort criteria, max results, etc. But we may also be dealing with UI elements which allow the user to re-order this data, for example:




If the user changes something here, we shouldn't be going back to the API with a second call just to get the same data in a different order. A much better solution is to always pass the data through a sorting function which picks up those parameters on the way to the the rendering function.

In the past I've done this like so:

var render = function(data){
  ... create some markup and insert into DOM ...
};
var sort = function(data){
  var container = $("#chart"),
      sortBy = container.find('input[name=sortBy]').val(),
      sortOrder = container.find('input[name=sortOrder]').val(),
      maxResults = container.find('input[name=maxResults]').val(),
      truncate = container.find('input[name=truncate]').val();
  ... sort/truncate/etc ...
  return sortedData;
}
render(sort(data.response.results), $("#chart"));

This is a lousy idea, and I did it for years. The problem is that it requires both the sort() and render() functions to know enough about the DOM to find and read the UI elements specifying sort, maxResults and so on, and where they're supposed to render the resulting markup. A better solution is to do something like this:

var params = function(container){
  return {
    "sortBy": container.find('input[name=sortBy]').val(),
    "sortOrder": container.find('input[name=sortOrder]').val(),
    "maxResults": container.find('input[name=maxResults]').val(),
    "truncate": container.find('input[name=truncate]').val()
  }
};
var filter = function(data, filterCriteria){
  ... sort/truncate/maxResults ...
  return sortedData;
};
var render = function(data){
  ... create some markup ...
  return markup;
};

var container = $("#chart");
var chartType = container.attr("class").eq(0);
container.append(render[chartType](filter(data.response.results, params(container)));

params(), filter() and render() are all abstracted from the DOM now, and can be reused anywhere in your application, provided you keep your naming conventions the same everywhere.

Now let's talk about that chart() function.

In most web applications you're going to have to be able to handle more than one method for data visualization. You may be using flot for graphs, a jQuery plugin for sparklines, and Underscore.js for table markup. Each of these is likely to use a different data format, but because we've normalized our data we're starting in the same place for each of them.

A good way to handle this is to do something like this:

var render = {
  chart = function(data, options){
    ...format data for flot...
    var container = $("div");
    $.plot(container, formattedData, options.chartType);
    return container;
  },
  sparkline = function(data, options){
    ...format data for sparkline...
    var container = $("div");
    container.sparkline(formattedData, options);
    return container;
  },
  table = function(data, options){
    ...format data for table...
    var markup = _.template(options.template);
    return markup(data);
  }
}

Two things to note here: we're using jQuery to create a <div>, but we're not doing any DOM manipulation--we're simply returning that object to the calling function. Second in some cases we'll need to pass an "options" parameter along when calling render(). For example, when using the underscore.js "template" method, it's nice to be able to pass in your template markup from the page itself, like so:

var container = $("#chart");
var chartType = container.attr("class").eq(0);
var options = {template: $('#table_template').html()};
container.append(render[chartType](filter(data.response.results, params(container)), options);

In the next post, I'll bring this all together in a working example.

No comments:

Post a Comment