Dancing with Data

Last Sunday I gave a small workshop at the MapBox office, where I showed new findings I’ve discovered while working on my last visualization project. We turned a collection of countries into a dance of dots, each dot referring to a country in the collection. In other words: We created a simple animated Scatterplot.

The goal was to implement a data-driven visualization where the output image is determined by the data and visualization state, such as filters and selected variables. Once users modify either the state or the data, the visualization gets updated accordingly using smooth transitions between states.

image

We applied fundamental concepts such as Data Joins (a technique introduced by D3.js), Scales and Animation. For our example we were using Dance.js, a minimal visualization framework I’ve built (which is essentially just a flavor of Backbone.js) as well as Data.js for representing and accessing domain data.

Representing domain data

We are describing our domain data using a Data.js Data.Collection, which is very easy to use.

var countries_data = {
  "type": {
    "_id": "/type/country",
    "name": "Country",
    "properties": {
      "name": {"name": "Country Name", "type": "string" },
      "languages": {"name": "Languages spoken", "type": "string" },
      "population": { "name": "Population", "type": "number" },
      "gdp": { "name": "GDP per capita", "type": "number" }
    }
  },
  "objects": [
    {
      "_id": "at",
      "name": "Austria",
      "languages": ["German", "Austrian"],
      "population": 8.3,
      "gdp": 41.805
    },
    {
      "_id": "de",
      "name": "Germany",
      "languages": ["German"],
      "population": 82,
      "gdp": 46.860
    },
    ...
  ]
}

The Interface

We wanted our visualization to be easily embeddable into web-applications. So here’s how creating a scatterplot instance, using an arbitrary dataset looks like:

var countries = new Data.Collection(countries_data);
var scatterplot = new Scatterplot();

// Display the scatterplot by showing GDP (x-Axis) and Population (y-Axis)
scatterplot.update(countries, ["gdp", "population"]);

// Switch axes and refresh the scatterplot
scatterplot.update(countries, ["population", "gdp"]);

Layout and Refresh

Most visualizations can be implemented efficiently by separating them into two separate phases. In the first phase positions of the visual marks are calculated while in the refresh phase the actual drawing takes place. Here’s the implementation of our update function, which accomodates that pattern. It gets called every time the visualization needs to be updated.

function update(items, properties) {
  this.data["items"] = items;
  this.layout(properties);
  this.refresh();
}

Layout

In order to turn domain objects into dots we need to apply a scatterplot layout algorithm. The first part of the function sets up scale functions for the x and y-Axis, since we want to map our domain values (which can be very big numbers) to limited pixel space. The second part just iterates through all items we have in our collection in order to set the x and y positions by passing the values to the corresponding scale function.

function layout(properties) {
  var that = this;

  // Prepare scales
  function aggregate(p, fn) {
    var values = _.map(that.data["items"].objects, function(i) { return i.get(p); });
    return fn.apply(this, values);
  }

  var minX = aggregate(properties[0], Math.min);
  var maxX = aggregate(properties[0], Math.max);

  var minY = aggregate(properties[1], Math.min);
  var maxY = aggregate(properties[1], Math.max);

  function x(val) {
    return (((val-minX) * $('#canvas').width()) / (maxX-minX));
  }

  function y(val) {
    return (((val-minY) * $('#canvas').height()) / (maxY-minY));
  }

  // Apply layout
  this.data["items"].each(function(item, key, index) {
    item.pos = {
      x: x(item.get(properties[0])),
      y: y(item.get(properties[1]))
    };
  });
}

Refresh

During the refresh phase, elements are actually drawn on the screen. We need an individual strategy for placing new marks, update the positions of existing marks and remove marks that no longer exist in the dataset.

Enter / Update / Exit

Much like in the spirit of D3.js, we specified transformations, based on data-changes to update our visualization. There are three different cases we need to take care of: The updating nodes to modify, the entering nodes to add, and the exiting nodes to remove. The process where new, existing and exiting nodes are determined is called a Data Join.

image

With Dance.js we are able to specify transformations for each case separately:

var collections = {
  "items": {
    enter: function(items) {
      items.each(function(item) {
        var dot = $('<div class="dot" id="'+htmlId(item)+'"></div>')
                     .css('left', Math.random()*$('#canvas').width())
                     .css('bottom', Math.random()*$('#canvas').height())
                     .css('width', 1)
                     .css('height', 1);
        $('#canvas').append(dot);
      });

      // Delegate to update (motion tweening fun)
      _.delay(this.collections["items"].update, 200, items);
    },

    update: function(items) {
      items.each(function(item) {
        var cell = $('#'+htmlId(item))
                     .css('left', item.pos.x)
                     .css('bottom', item.pos.y)
                     .css('width', 10)
                     .css('height', 10);
      });
    },

    exit: function(items) {
      items.each(function(i) { $('#'+htmlId(i)).remove() });
    }
  }
};

Results

Finally this is how our result looks like. It’s kept very minimal intentionally, as this example should just serve as a starting point.

There’s another first time dance you might want to look at, the Barchart Dance.

Dance.js and Data.js

You could use other tools or even no framework to apply the same techniques. I just chose this lightweight toolset as I’m using it in my projects and it suffices for many tasks. There are more major tools around, such as the D3.js visualization framework. Actually Dance.js works well in conjunction with D3. It gives you the joy of both Backbone and D3. Instead of Backbone.Model it uses Data.js for representing your domain models. Just ping me on twitter if you have any questions.

Start experimenting

Feel free to create your own variations and further improve the code. Let me know if you’d like to share your work. You can access the
full source code at Github. Probably the easiest way to get started is to check out Dance.js and look at the examples.

git clone git://github.com/michael/dance.git
cd dance
git submodule init
git submodule update

References:

Comments are closed.


Dudes

Projects