Responsive and Dynamic Visualizations with D3.js

Responsive and Dynamic Visualizations with D3.js

  • 558

D3 is a fairly meaty library to dig into, and while it does save a lot of work when it comes to rendering, scaling, and some mathematical computations, it is still largely up to the user to understand how the data needs to be handled and manipulated to render charts properly.

Introduction

It’s no secret that a lot of people out there really, really, don’t like math. It was the truth as I was growing up, it’s the truth now, and I don’t believe it’ll ever stop being the truth. As a kid who grew up under the standard K-12 education system, one of my strongest subjects throughout the years was always mathematics. Memorization was less important than conceptualization and by understanding numbers and relationships, answers could always be constructed rather than regurgitated. I enjoyed it, and I was great at it. At the same time, I understood that not everyone felt the way I did about numbers.

I sympathize — numbers and math can be hard. As my education advanced into my undergraduate degree, I met people majoring in CS, math, and physics doing problems that made me want to pull my hair out. Math can be difficult, numbers can be extremely abstract, and not everyone enjoys spending hours staring at a piece of paper to tease out some niche relationship (I mean who would, right?)

That being said, as we move into an age of information dominated by technology and computation, handling and interpreting data becomes an increasingly necessary skill to have. Computers store and manipulate data in a way that’s generally unfriendly to human readability. Being able to extract and interpret this data is so important that there’s an entire data science field growing up around the topic.

What does any of this have to do with D3? It comes down to this: Data is numbers (or at least it can usually be digested to a numerical format), and numbers can be gross….

This is image title

But pictures…pictures can be much less gross. In an age where data and mass information interpretation is becoming more and more critical for performance in every field, visualization of data for ease of interpretation by everyone is an extremely powerful toolset. What better way to represent data visually, than with graphs!

Most graphs are simpler than this one.

Most graphs are simpler than this one.

Well made data visualizations make large datasets accessible and interpretable for everyone. Libraries like D3 give us powerful pre-made toolkits for manipulation of data into visually representable formats. On that note, let’s move into more of the meat of D3 — what is it and what sort of tools does it provide for data visualization?

What is D3?

D3 is a JavaScript library that provides functionality for visually representing data through Html, SVG, and CSS. In other words, it’s a method library for drawing pictures based on data (although it can do much more than just pictures). Using it in your code is as simple as including it alongside any other JS script tags you run.

<script src="https://d3js.org/d3.v5.js"></script>

D3 provides a wide array of features, including external file parsing into JSON (.csv, .js, etc), HTML table generation from arrays, binding datasets (arrays) to DOM selections and dynamically rendering DOM elements. It also takes many processes involved in ‘vanilla’ DOM traversal and streamlines them into dry compact methods. These are all just the tip of the iceberg when it comes to D3’s functionalities, but for starting out with this library, I think the most important ones to grasp are the last three: binding data, dynamic elements, and streamlined DOM traversal.

Binding Data

First and foremost D3 allows you to bind data (arrays) to DOM element selections for later dynamic rendering. What does that mean exactly?

To start, you would make a selection of elements from the DOM. This is where some of D3’s streamlined traversal comes in handy. D3 provides an example of a messy traversal on their homepage, with code to change all p tag colors to blue:

This is image title

This is not fun to write, so D3 provides us with a way to streamline this selection alongside the attribute change. The above has the same functionality as the following:

d3.selectAll("p").style("color", "blue");

The selectAll method returns a selection of all relevant elements that are downstream of the provided node. If you provide d3.selectAll(), you get everything on the document. Running namedDivVariable.selectAll() selects everything downstream of the div in question. This makes DOM selection and attribute change much simpler — thanks D3! Now that we have ways to quickly retrieve “selections” of elements, we can bind our data to them. Let’s provide an example.

Say we have three arrays with different numbers of elements: three, two and one.

let threeElement = [true, false, true]
let twoElements= [true, false]
let oneElement= [true]

On the DOM we also have two circle SVGs. Don’t worry about the correct SVG syntax for now — just assume they’re proper elements. These circles also have a possible attribute: crossed; true — Ø, false — O. We can go ahead and select these and store them in a circles variable.

circle1 = O, circle2 = O
let circles = d3.selectAll(“circle”)

Now we can bind this array data to our selection of elements. What this means is that each value in our data array gets connected to each element in our selection by D3 magic. These data values can later be accessed functionally to dynamically render our elements. But we’ll get back to that later. The way that we bind data is through the .data() method. So we can bind our array of two values to our two circles like so: circles.data(twoElements). Now, the first circle is joined to our first data element ‘true’, and our second circle is bound to the second element ‘false’. This is called the update selection — when the number of data values matches the elements in the selection.

Update Selection:
circles.data(twoElements)
//Gives back all elements and matching data bound
circles.data(twoElements).attr(‘crossed’, function(d){return d})
Would result in something like // circle1 = Ø, circle2 = O

In the code above, you’ll notice some mess in the .attr() method where I’m setting the circle to be crossed or not. Don’t worry about that for now. Just know that crossed is being set to the value of the data element that has been connected to each circle.

The update selection is all well and good when we have an element to match each data value, but what if the number of data values and elements in the selection don’t match? D3 also supports two other types of selections that incorporate overflow in either direction: .enter() and _.exit()_. Here’s how they work.

With the .enter() selection, you receive any DATA values that did not have a matching element. These unmatched values can be used to render new elements corresponding to them with .append().

Enter Selection:
circles.data(threeElements).enter()
//Gives back all Data Values that don’t have a matching circle
//e.g. [true] will be unmatched
circles.data(threeElements).enter().append(‘circle’).attr(‘crossed’, function(d){return d})
Would result in something like // circle1 = O, circle2 = O, circle3 = Ø

Here, we see that a three-element array is being bound to our two circles. Circle1 gets true, circle2 gets false, and our last true is floating. When we call .enter(), that floating element is now what we have selected. For each of these floating data points, we can .append() a new circle and set its crossed attribute to the now assigned data point true. By the end, we haven’t changed any of our original elements, we’ve only entered the floating data points in as new circles.

This concept was tricky for me to grasp at first, but once you understand the enter selection, you gain access to one of the most powerful tools of D3. Say, for example, I selected something I know to not exist. I then have an empty selection. I can bind any data array to it, then use .enter() to generate as many elements as I have data, all corresponding to a single data point. This is one of the most useful tools for D3 chart generation and a powerful one to master.

Finally, we have the .exit() selection. It is the opposite of the enter selection. The .exit() method will return all the elements that do not have matching data values.

Exit Selection:
circles.data(oneElement).exit()
//Gives back all elements without Data to bind to
//e.g. [circle2] will be unmatched
circles.data(oneElement).exit().remove()
Would result in something like // circle1= O

What happens here is that the one element array of data is bound to our two element selection of circles. The first element gets bound to true, the second one does not get bound. Then, with .exit() we can access any elements that don’t have a data element bound to it (e.g. circle2). Calling .remove() on it will delete that circle from our DOM, leaving us with only an unchanged circle1.

This is a lot to consume at once, but these three functionalities are the meat of D3’s DOM traversal and data binding functionalities. They take some practice and some debugging, but once you’re used to them they’re remarkably flexible. Now that we are able to traverse the DOM and bind data to our selected elements, we reach the second critical component of D3: dynamic rendering.

Dynamic Rendering

This feature is possibly the hallmark of D3 and the primary reason you would use the library. D3 gives us the ability to generate and render HTML elements conditionally based on data arrays. We can access data values bound to HTML elements and derive the elements’ attribute values from the data functionally.

What this means is that if I have an array of numbers and select all rectangles (where none exist), then use .enter() and then .append(“rect”) to create a rectangle for each data value, I can functionally set each rectangle’s height to be equal to its corresponding data value. I can also set its location dependent on its index in the selection. Suddenly, I have a bar graph.

This:
thing2.js

let barArray = [26.76, 53, 53, 26.76]
canvas.selectAll("rect")
      .data(barArray)
      .enter()
        .append("rect")
        .attr("width", 10)
        .attr("y", 100)
        .attr("height", function(d){
          return d
        })
        .attr("x", function(d, i){
          return i*60
        })

Becomes something like this (without the labels and dress-up of course):

This is image title

From a personal project of mine in progress: A graphing application that parses CSVs and generates graphs from selected columns/rows, written almost entirely in JS, Rails API backend, Implements D3.

Let’s break down what’s happening in this render:

  1. I retrieve an empty selection under canvas.
  2. I append my four data values to the empty selection.
  3. Using enter, I access all the floating data without elements.
  4. For each, I append a rectangle and set its width to 10, and its y position to 100. Note: This is not how canvas actually works with respect to Y positioning, but just for the sake of explanation.
  5. I set each rectangle’s height, the value of which is this rectangle’s data value.
  6. Similarly, I can use the index as the second argument of the function in i and set the x position dependent on its index. First element at x=0, then x=60, then x=120 and so on

This is where the unlimited possibilities of D3 start to open up to us. We’re able to generate any kind of element in response to collections of data and specify how they are rendered based on their bound information. This flexibility can be extended to any type of visualization. The best part is that D3 has hundreds of helper methods and functions to work out the more difficult aspects of graph generation for us.

Features like scaleLinear() allow us to translate a dataset from one spread to another in a linearly. For example, an array from 0 to 500 could be converted linearly and accurately to an array from 0 to 100 for percentage-based rendering.

thing2.js

let data = [50, 0, 256, 500, 480]
let heightScale = d3.scaleLinear()
                  .domain([0, 500])
                  .range([0, 100])
rectangle.attr("height", function(d){
                 return `${heightScale(d)}%`
              })

Methods like transition() allow us to easily set up animation CSS for our elements as part of their rendering. The code below will render the circle at the top left corner, wait one second, then move it to the bottom right corner over two seconds.

thing3.js

circle.attr("cx", `0%`).attr("cy", `0%`)
circle.transition()
      .delay(1000)
      .duration(2000)
      .attr("x", `100%`)
      .attr("y", `100%`)

Using scales, D3’s axis functionality allows us to generate properly scaled axes to append to our charts for ease of interpretation. Using the same array and scale from above:
thing4.js

let data = [50, 0, 256, 500, 480]
let heightScale = d3.scaleLinear()
                  .domain([0, 500])
                  .range([0, 100])
let yAxis = axisRight(heightScale)
canvas.call(yAxis)

Once you practice them, every one of these methods works compactly, smoothly, and relatively intuitively when writing out the code. The size of our arrays becomes arbitrary, and everything is rendered conditionally and appropriately based on our data. This results in precise, accurate, and clean visualizations of data that are as dynamic as our code. Coupled with D3’s extensive styling and axis functions, we can interpret arbitrarily large datasets in ways that are much more digestible and appealing to the eyes. A large table of transactions, checking balances and savings over time can be converted into a much more appealing line graph:

Conclusion

D3 is a fairly meaty library to dig into, and while it does save a lot of work when it comes to rendering, scaling, and some mathematical computations, it is still largely up to the user to understand how the data needs to be handled and manipulated to render charts properly. That can be difficult to do and I had my fair share of fun debugging my mistakes with this library my first time learning it. I would, however, describe learning it as somewhat like riding a bike. Once you get going, the code is remarkably intuitive, and you’ll outpace every pedestrian charting in vanilla JS by miles. It is currently my go-to JS library for data visualization and one of my personal favorite tools to experiment with.

A quick glance at the D3 Examples Gallery will give you a better idea of what experts and teams are able to construct with this library. If that doesn’t make you fall in love with graphs, nothing will!

Graphing and data sifting are not always enjoyable but in an age of information science, they’re always useful. Plus, D3 is one of the more popular libraries out there for JS visualization, so there’s a large number of resources and tutorials available to help you learn all its features.

I personally recommend this youtube series by d3Vienno to start. It’s a great series that goes through the basics of rendering and data binding, and while some of the syntax he uses is changed in newer versions, the vast majority of it is consistent. It’s a great series for a first-start code-along.

For any of you who made it this far in reading, I hope I’ve been able to help you understand the basics of the D3 library. More importantly, I hope that I’ve inspired some of you to take up this library yourself and experiment with large dataset visualization. Math can be hard, numbers are gross, but pictures are pretty. With hard work and patience, unpleasant tables, numbers, and data collections can be turned into something beautiful to look at. Something readable, and enjoyable by all.