March 13, 2016

Intro to D3.js and Data Visualization

edit

Recently, I worked my way through the D3 Intro Course taught by Ian Johnson over at Front End Masters and have been practicing what I’ve learned ever since. I’m not great at it (yet), and I haven’t made anything too amazing, but I am getting a better grasp of this awesome library (big thanks to creator Mike Bostock and all his posts/tutorials about it).

I am currently working on a project at FINE that utilizes D3.js, so I wanted to code up an example for you. This way, you can see some of the techniques I might use in the project and perhaps inspire you to try out D3.js in a future project of yours.

First things first, you can’t build a data visualization without first having some data to work with. Lucky for me, there’s data everywhere. I decided for this example, I would create a dataset based on the people I work with everyday.

To do this, I created a JSON file, turning each one of my fellow devs into a an object in an array. Normally, you shouldn’t objectify people, but I got their permission to so this time (Remember, there are ethics to responsible data visualization). Here’s a small snippet of that JSON file:

{
  "teammates": [
    {
      "id": 1,
      "name": "Charlie",
      "birthdate": "1988-10-17",
      "position": "DevOps",
      "startdate": "2015-07-06"
    },
    {
      "id": 2,
      "name": "Eman",
      "birthdate": "1986-09-16",
      "position": "Front End",
      "startdate": "2015-05-12"
    }
    // More teammates here...
  ]
}

You Can’t Do Data Visualization Without Some Data

Now I have some data to work with. I thought it would be interesting to create a bar chart that graphs each dev’s age and how long they’ve been at FINE. This would be represented on the X axis and with a radio button would toggle between the two. Later, I will add some sorting methods to the Y axis. Let’s start coding this up.

While we could code this visualization as a one-off piece of code, I think it’s a good idea to follow the “Reusable Charts” pattern laid out in this awesome blog post. In short, a reusable chart turns our visualization into a function that can be called on multiple datasets. This way, when our dataset is updated, we simply re-render the chart with the new data. To do this, we need some basic setup to our code.

if (!d3.chart) {
  d3.chart = {}
}

d3.chart.fine_visual = function () {
  var data, svg

  function chart(container) {
    // Setup static elements of our visualization
    svg = container

    // Call update function to apply our data to our visualization
    update()
  }

  chart.update = update
  function update() {
    // Code the dynamic elements of our visualization
  }

  chart.data = function (value) {
    if (!arguments.length) return data
    data = value
    return chart
  }

  return chart
}

d3.json('teammates.json', function (err, teammates) {
  if (err) {
    return d3
      .select('.js-display')
      .append('p')
      .text('There was an error retrieving the data')
  }

  var data = teammates.teammates
  var display = d3.select('.js-display')

  var visual = d3.chart.fine_visual().data(data)
  visual(display)
})

The code starts by ensuring that the d3.chart object exists so that our custom function can be set as a property on the d3.chart object. This is followed by var declarations that will be scoped to our function. Next is the chart function which will be returned when this visual function is called. This is followed by setting a property on our chart function of our update function (so that we can call updates on our chart). Lastly, any chart property that can be customized by our end user needs this chart.property pattern to create getters and setters for the property.

Once the custom chart function is created, an AJAX call to the data is made. Upon returning the data, the custom function is called with the data supplied to it with the .data() setter.

The Chart() Function

The chart function within our custom function is where pieces of our visualization that will remain static should be instantiated. If this code is instead put in the update()function, it will be re-rendered to the page (meaning multiple axes would pile on top of each other). Thus, it follows that pieces of the visualization that will be updated are moved into the update() function.

An example of static content is an axis. A bar chart will have an X and Y axis, representing some scale of our data. In the case of my example, the X axis will represent time measured in months, and the Y axis will be used to identify different developers.

In order to add an axis to our visual, a decision must be made regarding what scale to use. D3 provides a number of different scale types, saving you from the hassle of coding your own (though you can if you want to). In this case, the linear scale will suit our purposes, but we could have also considered using the time scale as well. Be sure to read about d3 scales when you have a chance.

To implement the scale and append an axis based on it, the following code is used:

var xScale = d3.scale
  .linear()
  .domain([
    0,
    d3.max(data, function (d) {
      return age(d.birthdate)
    }),
  ])
  .range([0, width])

var xAxis = d3.svg.axis().scale(xScale).orient('bottom')

var xAxisGroup = svg.append('g')

xAxis(xAxisGroup)

I have left out some code on purpose here, namely the helper function age(), in order to focus on the more important pieces. A linear scale is declared, the domain is set from age 0 to the maximum age of our dev team, and the range is from 0 to the full width of our visualization. Then, an axis is declared and called on the svg var in an appended g element. The example follows a similar pattern for the Y axis, but uses the ordinal scale instead. I may cover the sorting functionality of the Y axis in depth in another post.

The D3 Pattern: Selecting, Entering, Appending, Exiting, Removing

Now that the axes are in place, the bars can be added to our chart. D3 has a pretty specific pattern to follow when rendering data driven elements to our visual. It seems downright counterintuitive at first, but begins to make sense the more you understand how d3 is acting upon our objects. This pattern looks similar to this:

var bars = svg.selectAll('.bar')
  .data(data);
  .enter()
  .append('rect').classed('bar', true)
  .attr({
    x: 0,
    y: function(d,i) { return i * 35; },
    width: function(d) { return xScale( age(d.birthdate) ); },
    height: 30,
  })
  .exit()
  .remove();

While this isn’t the exact code used in the example, it’s a good example of showing this d3 pattern at work. Because d3 can update elements already on the page, it’s important that we select those elements. If not, we would end up creating a function that added a whole new set of elements each time the data was updated. This is why all elements are selected, then the data is entered, existing elements are updated, new ones are added and unused ones are destroyed.

Customizable Settings

The last convention I want to cover is how to add configurable settings on our reusable chart via adding a property to the chart() function. You may have noticed above, that we had a small bit of code looked like this:

chart.data = function (value) {
  if (!arguments.length) return data
  data = value
  return chart
}

This bit of code allows our user to get and set the data property for our chart. This is what enables us to reuse this chart in another instance or update our current chart by setting a new dataset. Typically, this is used on width and height properties to allow the user to customize the size of their visualization, but there can be many other uses.

Taking these concepts (and adding a few more I leave you to discover), a quick and interesting data visualization can be created. Play around with it a while and explore the code. Fork it on Codepen or make your own, and best of luck as you explore d3.js in the future.

See the Pen

d3 Intro - Example for Mingle post

by Kyle Shevlin (@kyleshevlin) on CodePen.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Need help with your software problems?

My team and I are ready to help you. Hire Agathist to build your next great project or to improve one of your existing ones.

Get in touch
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Agathist
Good software by good people.
Visit https://agath.ist to learn more
Logo for Just Enough Functional Programming
Just Enough Functional Programming
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.