~ read.

One Approach to Lining Up, Scattering, and Spiraling Data Circles in d3.js

Sometimes, you just want to do cool things with your data. When I saw that wikipedia had a page listing the alcohol consumption per capita by country, my first thought was: the world will be a better place if this was visualized in d3. For those of you that are new to d3, it's a javascript library that helps you manipulate and visualize data with HTML, SVG objects, and CSS. D3 enables you to create collection objects comprised of DOM node and data object pairs. This way you can manipulate the DOM nodes using the data attached to it. With that said, I used d3 to create this (see how below):

The Setup:

The first step was to upload the data so that it can be used in a d3 collection object. I decided to use a csv file given the convenient csv function that d3 provides. It's as easy as:

d3.csv(/path/to/file, function(csv){...});

The d3 built in support for parsing csv files takes two parameters, the path-to-file and a function to make use of the parsed data. The parser automatically makes the first row in the csv keys for the rest of the values in the rows like this:

Once I got that working, I decided to use SVG circles to represent the data objects, and used the following to create the collection object:

var circles = d3.select('svg').selectAll('circle').data(csv);
circles.enter();

The above code lets me grab a previously created SVG node and append a 'circle' for each row of data on the csv to it, and that's not all. Now that I've attached a data object to each circle, I can begin manipulating the circle's attributes based on the data. For example, I can make a circle's radius equal to the total alcohol consumption per capita of the circle's respective country by doing the following:

d3.selectAll('circle).attr({
    // the d is the attached data object and 
    // Total is a key on that object
    r: function(d) {return d.Total}; 
});

Lastly, I wanted to make all the data-circles draggable. D3 provides a really easy way to do that:

var drag = d3.behavior.drag()
  .on('drag', dragMove);
d3.selectAll('circle').call(drag);

Now, lets get into the titular stuff:

Lining up the data circles:

I decided to line the data up to resemble the shape of an spirit bottle. In order to do this, I needed to assign each of collection pairs an id dynamically based on which category was being viewed (i.e. spirits, wine, beer). Once I got that down, I created the following function:

var createPositions = function(cx, cy, circlCollection){
  var diameterPadding = 35;
  cx[0] = 300;
  cy[0] = 20;
  for (var i = 1; i < circleCollection.length; i++){
    var lastPosition = cx[i-1];
    cx[i] = lastPosition + diameterPadding;
    cy[i] = cy[i-1];
    if (cx[i] > (width-300) && cy[i] < 190) {
      cx[i] = 300;
      cy[i] = cy[i-1] + diameterPadding;
    } else if (cx[i] > width-300 && cy[i] === 195) {
      cx[i] = 100;
      cy[i] = cy[i-1] + diameterPadding;
    } else if (cx[i] > (width-100)) {
      cx[i] = 100;
      cy[i] = cy[i-1] + diameterPadding;
    }
  }
};

The basic idea is that the function takes in two empty objects that take in the values of the cx and cy for each circle with a key of the category's consumption per capita(cx and cy are x,y positions for SVG objects that are relative to the SVG). The third argument is the the circle collection that I want to assign cy and cx values to. I would then assign a cx and cy for the next circle based on the previous circle's position. In order to create that spirit bottle shape that I was looking for, I just started new rows at absolute points since the SVG object has an absolute width and height.

Scattering the data.

This one is really simple, just take the width and height of the SVG object and apply a random value to the cx and cy attribute of the circle:

circles.attr({
    cx: function(){return Math.random() * width;},
    cy: function(){return Math.random() * height;}
});

Now for the grand finale, the Spiral.

I had my fellow @hackreactor classmate - @faridonfire brush me up on some high school trigonometry to get this working. In short, we took the angle of each circle based on it's id (sorted by total consumption per capita) and multiplied it by the total degrees in the radial spiral we were looking to make in order to find the 'theta'(in this case 8*Pi). We then multiplied that by the sin/cos of the angle to dictate the cx and cy coordinates, thus giving us that pretty spiral that you can see above by clicking on the spiral tab. Here's the code:

var createSpiralPositions = function(cx, cy, sortedCirc) {
  cx[0] = width/2;
  cy[0] = height/2;
  var ringRadius = 200;
  var degrees = 8*Math.PI;
  for (var i = 1; i < 188; i++){
    var angle = i/188 * degrees;
    var y = (ringRadius * i/188) * Math.sin(angle);
    var x = (ringRadius * i/188)* Math.cos(angle);
    cx[i] = x + cx[0];
    cy[i] = y + cy[0];
  }
};

If you haven't already realized, d3 is insanely powerful. I'm excited to continue exploring new ways of rendering browser visualizations with d3 and creating immersive experiences with otherwise boring data sets. If you have any cool ideas, tweet at me @JoshSGman.

comments powered by Disqus