Chris Essig

Walkthroughs, tips and tricks from a data journalist in eastern Iowa

Archive for April 2015

Exploring D3.js

leave a comment »

network_graph_mary_ssI’ve been wanting to use the D3.js library for some time now but I haven’t up to this point mostly because the code that runs the charts scares the crap out of me (that’s only a slight exaggeration).

Well this weekend I finally took on the scary and published my first D3 chart. The final product is a network graph that shows how one woman is helping connect people within the community. It’s not crazy awesome like most of the other D3 graphics out there but it’s a start.

The code started out as this chart from Mike Bostock and was modified to meet my needs. The final code is on GitHub.

While it wasn’t quite as intimidating as I thought it would be, a few things tripped me up. Chaining methods in D3 was something that threw me off at first. I was also unfamiliar with SVGs. This is a good primer from Scott Murray. Finally, many of the D3 methods are still mysterious me, like diagonal and the layouts. But one step at a time, right?

Some of it, however, isn’t crazy complicated. I’m going to review some of what I learned about D3 by¬†going through some of the code that powered¬†this project.

The code snippet below is line 47 in the chart’s script.js file. It starts the graph. It selects the empty SVG that I put in index.html, sets it’s width and height and appends a G element to it:

// Create an svg
  svg = d3.select("svg")
  	.attr("width", width + margin.right + margin.left)
  	.attr("height", height + margin.top + margin.bottom)
  	.append("g")
  	.attr("transform", "translate(" + (margin.left - line_width) + "," + margin.top + ")");

A few lines down, we create a variable called node, select all the G elements and bind data to them.

// Update the nodes
  var node = svg.selectAll("g.node")
  	.data(nodes, function(d) {
      return d.id || (d.id = ++i);

  	})

This G element contains everything in the chart and is placed just under the empty svg element in index.html

We then create a variable called nodeEnter, which appends another G element to the G element we just created. (confusing, I know).

// Enter any new nodes at the parent's previous position.
  var nodeEnter = node.enter().append("g")
    	// Set location of nodes
    	.attr("transform", function(d) {
    		if ( d['name'] === 'Mary Palmberg' && d['depth'] === 2) {
    			return "translate(" + circle_mary_location + "," + circle_mary_translate + ")";
        } else {

          
          if ( d['name'] === 'Thomas Jensen' ) {
            d.x -= 20;
          } else if ( d['name'] === 'Clarence Borck' ) {
            d.x += 20;
          } else if ( d['name'] === 'Jane Skinner' && d['connection'] === 5 ) {
            d.x -= 20;
          }
    			
          return "translate(" + d.y + "," + d.x + ")";
    		}
    	})
    	.attr("class", function(d) {
    		if (d['name'] === 'Mary Palmberg') {
    			return 'node-mary';
    		} else if (d['name'] === 'Top level') {
    			return 'node-top';
    		} else {
    			return 'node'
    		}
    	});

In our chart, these G elements are the nodes that contain our circles. Later in the code, I made lines that connect the circles. But we’re going to focus on just these G elements.

It may make more sense to inspect the graph to see what I’m talking about:

d3_mary_blog

After we append the G elements to their parent G elements, we chain a series methods to it, which set attributes for the G elements. These methods are called D3 selectors. With D3 selectors, you can select any shape in the SVG and a do a bunch of things to it. We can append new nodes to it, set CSS properties, give it a certain class name, etc. And since we bound data to the shape, we can decide what those properties will be based on the data we gave it.

This is seriously awesome, by the way.

In the code above, we used a D3 selector to set the location of the shapes using the CSS attribute transform. You’ll notice how setting that attribute is a function with a “d” parameter. “D” is equal to the data behind the shape we are drawing (remember, we appended data to it earlier). It returns an object with different properties. In this case, the property “name” is equal to the name of the person we are drawing in the circle.

For this chart, I had 16 G elements on the page. I also have objects with properties about each G element in a separate JSON file. As we loop through each G element and create it on the page, the data parameter becomes whichever G element we are on. So when we are creating Mary Palmberg’s circle, the “D” parameter will equal data about Mary Palmberg.

“D” returns an object that looks like so:

d3_mary_blog02

In the code, I’m checking to see if the G element we are on is Mary Palmberg, because her circle is placed in the middle and all other circles point to her. If we are on Mary, we set the CSS property transform to the variables called “circle_mary_location” and “circle_mary_translate”, which I set earlier in the script.js file. These basically put her circle in the middle of the page.

If it’s not Mary, we run the else part of the if-else statement. We do a couple of checks on a few people and change their x values. This is done because some of the circles were a little bit too close to together. After that, I put the data’s x and y coordinates in CSS translate property and return it. This basically puts it in the correct position on the page.

The last part should be easy to understand. All it does is set a class for the G Element depending on the person’s name. We give Mary a different class name so we can apply different styles to her circle:

d3_mary_blog03

It’s important to note that we haven’t actually created any shapes yet. Instead, G elements are just groups of shapes. The next thing we’re going to do is actually append circles to the empty G element.

nodeEnter.append("circle")
    	.attr("id", "circle-outer")
    	// Make radius smaller than inner circle
    	.attr("r", function(d) {
    		if (d['name'] === 'Mary Palmberg') {
    			if ( $(window).width() > 700) {
                    return circle_radius + 7;
                } else {
                    return circle_radius + 17;
                }
    		} else {
    			return circle_radius + 2;
    		}
    	})
    	// Color code the border around the circle
    	.style('stroke', function(d) {
    		// Color Mary differently
    		if (d['name'] === 'Mary Palmberg') {
    			// Red
    			return '#e41a1c'
    		} else if (d['connection'] === 1) {
    			// Purple
    			return "#984ea3";
    		} else if (d['connection'] === 2) {
    			// Green
    			return "#4daf4a";
    		} else if (d['connection'] === 3) {
    			// Blue
    			return "#377eb8";
    		} else if (d['connection'] === 4) {
    			// Orange
    			return "#ff7f00";
    		} else if (d['connection'] === 5) {
    			// Brown
    			return "#a65628";
    		} else {
    			// Blue
    			return "#377eb8";
    		}
    	});

  // Create the inner circle
  nodeEnter.append("circle")
    	.attr("id", "circle-inner")
    	// Radius
    	.attr("r", function(d) {
    		if (d['name'] === 'Mary Palmberg') {
    			if ( $(window).width() > 700) {
                    return circle_radius + 5;
                } else {
                    return circle_radius + 15;
                }
    		} else {
    			return circle_radius;
    		}
    	})
    	.style("fill", function(d) {
    		if ( $(window).width() > 700) {
                return "url(#" + d['name'].replace(' ', '') + ")"
            } else if ( d['name'] === 'Mary Palmberg') {
                return "url(#" + d['name'].replace(' ', '') + ")"
            }
    	});

I appended two circles to the G elements. The first is the circle around a person. I gave it an id of “circle-outer” and set it’s radius (attribute “r”) to a variable called “circle_radius”, which is set at the top of the script.js file. Once again I did a check for Mary and gave her a bigger circle since she’s in the middle. I then set the color of the circle’s stroke based on data property “connection”.

Our network graph has five total connections, so there’s a possibility of five different colors. I set the connection number in the JSON file. This is done so every connection through Mary is color-coded the same and thus the reader can see they are part of the same story. Same color = same story. And Mary stays in the middle of it all.

The second circle is the image itself. It has an id of “circle-id”. The attribute fill is set to the image of the person. The urls for each image are set in index.html under the defs tag. Each image is a pattern with a “xlink:href” property set to the url of the image. More information on how this works can be found here.

After that, I append text to have their name appear the circle. And I used this library to create the tooltips. I won’t bore you with that code as well.

There’s a few more things going on in this chart but those are the basics. Or at least as I understand them.

As with learning any new technology, my advice is to pick a project that you want to be powered by D3 and go at it. I learned a lot more about D3 making my first chart than I have with walkthroughs, video tutorials, etc. You’ll never learn it until you get your hands dirty.

Written by csessig

April 1, 2015 at 4:42 pm

Posted in d3, Javascript