D3 Beginnings

Reference

Today we started learning D3. Lots more new info to process! I will compile this post as a catch-all reference guide over the next few days of instruction.

Quick Reference:

Installing

I don’t know why I assumed it would require a big installation to work with D3. Not so! It’s just a matter of including the JS library in your HTML:

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

Basic Selections

d3.select() // select one element
d3.selectAll() // select multiple elements

The basic selector methods select in the same way CSS and jQuery select, by .class, #id, or element. These methods return a selection object containing a _groups array and a _parents array.

If you want to access the elements themselves, use the node method (or nodes for multiple elements):

d3.selectAll("li").nodes(); // returns an array of li elements
d3.selectAll("li").node(); // returns the first li element

Selections can be manipulated with several methods:

  • .style(property [, newValue]) allows you to add CSS
  • .attr(attribute [, newValue]) allows you to change attributes
  • .text([newValue]) allows you to add/remove text
  • .html([newValue]) allows you to add/remove HTML
  • .append(tagName) allows you to add HTML elements & return a new D3 selection

For each of these methods, you can also place a callback function in place of newValue. This callback has a specific structure which is defined below.

If you don’t pass in any value, these methods will act as getters:

// Manipulate elements
d3.select("#page-title")
.style("background-color", "#000")
.style("color", "#fff")
.attr("class", "#new-class")
.text("This new text will replace the old!");

// Get values already ascribed
d3.select("#page-title")
.style("background-color") // #000
.style("color") // #fff
.attr("class") // list of classes
.text(); // whatever text is inside

Instead of getting classes with the attr method, it’s preferred to use the classed method instead. The first parameter of the method is a list of classes and the second is true if you want to add the list of classes to the selection, or false if the classes should be removed from the selection:

selection.classed("space separated list of classes", boolean);

Finally, the remove method also works as a selector and removes elements at the same time.

Event Listeners

selection.on("eventType", callback)

Note that only one event listener can be attached to each element; if you attach more than one, it will only run the last one.

You can also remove an event listener with null passed in as the 2nd parameter:

selection.on("eventType", null)

For the callback function, the d3.event property must be used inside of the event handler to gain access to normal event handling object properties. Here is an example of an form submission event handler callback function in action:

// On submitting form, add the value of the input to a new list item
// then clear the input

d3.select("#new-note").on("submit", function() {
d3.event.preventDefault();

var input = d3.select("input")
d3.select("#notes")
.append("li")
.classed("note", true)
.text(input.property("value"));
input.property("value", "");
});

Passing Data With D3

Here is a first look at passing data into the DOM for display. This work with an empty ul with an id of #quotes, and an array or objects var quotes which contains (you guessed it) movie titles and quotes.

d3.select("#quotes")
.style("list-style", "none")
.selectAll("li")
.data(quotes)
.enter()
.append("li")
.text(function(d) {
return d.quote;
});

A few things are going on here:

  • Select the unordered list and style it
  • Select all lis in the list…but there are none to start! D3 creates a selection object with empty nodes for these lis.
  • Use the data method to attach the quotes array data to placeholder __data__ nodes.
  • Use the enter method to create a D3 selection from the placeholder nodes.
  • Append the data to the li DOM elements (note: append must be after the parent element has been selected, otherwise the element in question will be appended to the html element)
  • And finally set the text to return the desired property from the data object with a callback function.

Also worth noting: once the elements have been added to the DOM, they can be selected and manipulated using normal D3 selectors, and they remain bound to whatever data they were created with. In the above example, we could select the lis to change the text to the film title for example:

d3.selectAll("li")
.text(function(d) {
return d.title;
})

D3 Callback Structure

Callback functions in D3 take two parameters: the first is the data that’s getting passed into the DOM, and the second is the index it’s being passed in at (not needed/shown above). This is the default structure any time a callback is passed into a D3 method.

Refactoring

The operation above could be refactored and expanded on to make a more visually compelling display:

d3.select("#quotes")
.style("list-style", "none")
.selectAll("li")
.data(quotes)
.enter()
.append("li")
.text(d => `"${d.quote}" - ${d.movie} (${d.year})`)
.style("margin", "20px")
.style("padding", "20px")
.style("font-size", d => d.quote.length < 25 ? "2em" : "1em");

Removing Data

Like enter(), there is an exit() method on D3 objects to remove data. By default data is bound by index, so it’s necessary to bind data to elements to remove items correctly.

For example, if there are 5 values and you only want to display three of them (lets say odd integers from 1-5), by default D3 will recognize that there are three elements to keep, but it will only keep indices 0, 1, and 2. Not what we want!

Instead we can bind the data to DOM elements by adding a key function as the second parameter to the data() method during the selection. In the refactored code above, we add all quotes to the DOM and style them. Now let’s select only certain quotes, bind the data to each DOM element, and delete the ones we don’t want:

var nonRQuotes = quotes.filter(function(movie) {
return movie.rating != "R";
});

d3.selectAll("li")
.data(nonRQuotes, function(data) {
return data.quote;
})
.exit()
.remove();

Merging Data / Update Pattern

When items are added to or removed from the DOM, they are stored separately from items that were already in the DOM. This refers to the selection types:

  • Enter selection: data with no DOM elements attached
  • Exit selection: DOM elements with no data attached
  • Update selection: items with both data and DOM elements attached

To treat all of the items on a page as one, these separate storage areas need to be merged:

selection.merge(otherSelection)

This will create a new single selection with everything in it. All together, this makes up the general update pattern that is standard in D3:

  1. Grab the update selection, make any changes unique to that selection, and then store the selection in a variable.
  2. Grab the exit selection and remove any unnecessary elements.
  3. Grab the enter selection and make any changes necessary to that selection.
  4. Merge the enter and update selections, and make any changes you want to be shared across both selections.

Putting It All Together

To put it all of this (so far) together we coded a simple form which would display all of the unique characters in a string as a bar graph, where the height of the bar represents the number of times the character appears. It also stores the count from a previous string, but exits those items when a third string comes into the mix. This is the code I came up with (partly on my own):

const form = d3.select("form");
const input = d3.select("input");
const resetBtn = d3.select("#reset");
const phraseDisplay = d3.select("#phrase");
const countDisplay = d3.select("#count");

resetBtn.on("click", function() {
d3.selectAll(".letter").remove();
phraseDisplay.text("");
countDisplay.text("");
});

form.on("submit", function() {
d3.event.preventDefault();
// Create letter count object from input string
current = input.property("value");
let currentObj = getFrequencies(current);
phraseDisplay.text(d => `Analysis of: ${current}`);

// Attach new data to display div
let letters = d3.select("#letters")
.selectAll(".letter")
.data(currentObj, function(d) {
return d.character;
});

// Remove existing letters from 'new' and stage for removal
letters
.classed("new", false)
.exit()
.remove();

// Add new letters to display & style
letters
.enter()
.append("div")
.classed("letter", true)
.classed("new", true)
.merge(letters)
.style("width", "20px")
.style("line-height", "20px")
.style ("margin-right", "5px")
.style("height", function(d) {
return d.count * 20 + "px";
})
.text(function(d) {
return d.character;
})

countDisplay.text(d => `New characters: ${letters.enter().nodes().length}`);
input.property("value", "");
});

// From sorted string create objects with CHARACTER and COUNT for each unique character
function getFrequencies(str) {
let sorted = str.split("").sort();
let data = [];
for (var i = 0; i < sorted.length; i++) {
var last = data[data.length - 1];
if (last && last.character === sorted[i]) last.count++;
else data.push({character: sorted[i], count: 1});
}
return data;
}

The main part I had trouble with was handling the new vs. old string. In my first attempts I tried to store these values for comparison, but merging them was very convoluted and the walk-through showed a much better way (above).

I also tried a few different approaches for the getFrequencies() function, but ultimately created the currentObj in a way that didn’t work well with joining the data in D3: it was necessary to create an array of objects so that each object could be treated as a data entry. Creating a single object from the array ({h: 1, e: 1, l: 2, o: 1} etc.) made it much harder to join, trying to iterate through the keys. Actually, I couldn’t do it at all! So good to have the walk-through :)