D3: The Mighty Data (visualization)

Parts of this write-up come from the beautifully written scrimba course found here and also from the D3 docs

This is an intro to D3

What is D3.js?

  • An Open Source JavaScript Library
  • Helps creating visual representations of data
  • Uses HTML, CSS, and SVG
  • Viewable on modern browsers
  • An easy to use API

Some examples of what D3 can be used for are:

  • To generate an HTML table from an array of numbers
  • To create an interactive SVG bar chart with smooth transitions and interaction

Before D3 if we wanted to, for example, change the text color of paragraph elements, it would look something like this.

var paragraphs = document.getElementsByTagName("p");

for (var i = 0; i < paragraphs.length; i++) {  
    paragraphs[i].style.color = 'blue';
}

But with D3, we can make the same changes with much less, and more straightforward code:

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

That would select all <p> elements. You can also use id's, classes, attribute values, etc...

To select individual elements, rather than use .selectAll you would just use .select

d3.select("body").style("background-color", "black");  

Callback Functions

Many of the methods of D3 such as .style(), .text(), .delay(), .attr(), and many more accept a callback function as an argument.

If we want to randomly color all our paragraphs, we could do this

d3.selectAll("p").style("color", function() {  
  return "hsl(" + Math.random() * 360 + ",100%,50%)";
});

The callback function also accepts two arguments

function(data, index)  

Let's see how we can utilize this to our advantage.
Imagine we want to alternate shades of gray for even and odd nodes:

d3.selectAll("p").style("color", function(d, i) {  
  return i % 2 ? "#fff" : "#eee";
});

Notice that we didn't use the d or data argument, but we did use the i or index argument.

Now let's use the data argument

d3.selectAll("p")  
  .data([4, 8, 15, 16, 23, 42])
    .style("font-size", function(d) { return d + "px"; });

So the .data information will be what gets passed through the d argument.

This will give each paragraph a size based on the corresponding data.

Method Chaining

With D3 you can also do method chaining.

So if we had an HTML page that looked like this.

<!DOCTYPE html>  
<html lang="en">  
    <head>
        <link rel="stylesheet" href="index.css">
        <title>Learn D3.js</title>
    </head>
    <body>

        <h1>First heading</h1>


        <script src="https://d3js.org/d3.v4.min.js"></script>
        <script src="index.js"></script>
    </body>
</html>  

Then you could add the following into your JavaScript file

d3.select('body')  
    .selectAll('p')
    .data([1,2,3,4,5])
    .enter()
    .append('p')
    .text('D3 is awesome')

This would look like

We could try using a callback function with our data

d3.select('body')  
    .selectAll('p')
    .data([1,2,3,4,5])
    .enter()
    .append('p')
    .text('D3 is awesome')
    .text(d => d)

Which would change our page to look like this

or even more specifically

d3.select('body')  
    .selectAll('p')
    .data([1,2,3,4,5])
    .enter()
    .append('p')
    .text('D3 is awesome')
    .text(d => "This is number " + d)

Let's discuss what is happening in the above

.selectAll - Selects all instances of the specified tag, class, or id

.data - An array of the data or information to be used by callback functions

.enter - Creates the initial join of data to elements

.append - Specifies the type of element that should be created if there aren't enough already in existence for the each item of data

.text - Specifies what information to put inside of the appended element.

If we were to do the same thing but not include the .enter then it would just look like this.

This happens because our original HTML document didn't have any existing p tags. If we excluded .enter() but we had 2 existing p tags then it would include the first two elements of our data. Or if we had 3 then the first three elements would show up, etc...

HOWEVER,

If we had two existing p tags AND we still had .enter(), then the first two items of our dataset would be ignored.

Example:

   <body>

        <h1>First heading</h1>
        <p>Already Existing paragraph</p>
        <p>Already Existing paragraph</p>

        <script src="https://d3js.org/d3.v4.min.js"></script>
        <script src="index.js"></script>
    </body>
d3.select('body')  
    .selectAll('p')
    .data([1,2,3,4,5])
    .enter()
    .append('p')
    .text(d => "This is number " + d)

If you wanted to still use the .enter() AND keep the existing p tags with their content, you would do the following.

d3.select('body')  
    .selectAll('p')
    .data([1,2,3,4,5], d => "This is number " + d)
    .enter()
    .append('p')
    .text(d => "This is number " + d)

To understand better why .enter() acts this way, you can read this

Transitions

If we wanted to use transitions to make a small change at a controlled pace, we can use d3 for that as well.

d3.select('body').transition()  
    .duration(2000)
    .style('background-color', 'black')

Other D3 Methods

.attr - The first parameter is the attribute type we want to add, the second is whatever you want the attribute to be equal to.

  • Example - d3.select('input').attr('type', 'number') would add type='number' to an <input />

.style - Set or get a CSS style property for elements in the selection.

  • Example(get) - d3.select('h1').style('color') This would tell you the color of the h1 you selected.
  • Example(set) - d3.select('h1').style('color', 'blue') This would change the color of the h1 you selected to blue.

.duration - Specify transition duration for all elements. See above for example.

.exit - Return the selection of elements no longer associated with data.

For a full list of all the available D3 Methods, you can go here.