D3 Intro and Joins Notes

I’m relearning D3.js. Here are notes from different resources I’m reading.

Notes from the D3js.org Introduction.

Introduction

D3.js is a javascript library for manipulating documents based on data. It uses HTML, SVG, and CSS to do so. It emphasizes web standards so that you can use modern browsers without proprietary frameworks.

You can bind data to a DOM (Document Object Model) and apply data-driven transformations to the document.

Example: Generate an HTML table from an array of numbers, then use the same data to make an SVG bar chart.

Selections

D3 makes it a lot easier to modify documents than the W3C DOM API. The W3C method relies on verbose names and manual iteration. D3 employs a declarative approach that operates on arbitrary nodes called selections.

Here is Mike Bostock’s examples for how to change the text color of paragraph elements with the W3C method vs D3 method:

W3C:

var paragraphs = document.getElementsByTagName("p"); for (var i = 0; i < paragraphs.length; i++) { var paragraph = paragraphs.item(i); paragraph.style.setProperty("color", "white", null); }

D3:

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

Elements may be selected using a variety of predicates, including containment, attribute values, class and ID.

D3 provides multiple ways to change nodes: Setting attributes and styles, registering event listeners, adding/removing/sorting nodes, and changing HTML or text content.

Direct access to the DOM is possible because each D3 selection is a simple array of nodes.

Dynamic Properties

D3 has similar syntax to jQuery, but styles, attributes, and other properties can be specified as functions of data in D3, not just constants.

D3 provides built-in reusable functions and function factories, such as graphical primitives for area, line and pie charts

To alternate shades of gray for even and odd nodes:

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

Computed properties often refer to bound data. Data is specified as an array of values, and each value is passed as the first argument (d) to selection functions. With the default join-by-index, the first element in the data array is passed to the first node in the selection, the second element to the second node, and so on. For example, if you bind an array of numbers to paragraph elements, you can use these numbers to compute dynamic font sizes:

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

Once the data has been bound to the document, you can omit the data operator. D3 will retrieve the previously-bound data. This allows you to recompute properties without rebinding.

Enter and Exit

With D3’s enter and exit selections you can create new nodes for incoming data and remove outgoing nodes that are no longer needed.

When data is bound to a selection, each element in the data array is paired with the corresponding node in the selection. If there are fewer nodes than data, the extra data elements form the enter selection, which you can bring to life by using the .enter() selection. Example:

d3.select("body") .selectAll("p") .data([4, 8, 15, 16, 23, 42]) .enter().append("p") .text(function(d) { return "I’m number " + d + "!"; });

Updating nodes are the result of the data operator. If you forget the enter and exit selections, you will automatically select only the elements for which there exists corresponding data.

A common pattern is to break the initial selection into three parts: Updating nodes to modify, entering the nodes to add, and exiting the nodes to remove. Example:

// Update… var p = d3.select("body") .selectAll("p") .data([4, 8, 15, 16, 23, 42]) .text(function(d) { return d; }); // Enter… p.enter().append("p") .text(function(d) { return d; }); // Exit… p.exit().remove();

By handling the three cases (update, enter, exit) separately, you control precisely which operations run on which nodes.

D3 allows you to transform documents based on data. This includes creating and destroying elements.

D3 allows you to change an existing document in response to user interaction, animation over time, or even asynchronous inputs from a third-party. A hybrid approach is also possible, where the document is initially rendered on the server and updated on the client via D3.

Transformation, not Representation

D3 does not introduce a new visual representation. Instead, its graphical marks come from web standards: HTML, SVG, and CSS. If browsers introduce new features tomorrow, you can use them immediately with D3, no update required.

D3 is easy to debug with the browser’s built-in element inspector. The nodes D2 manipulates are the same ones the browser uses natively.

Transitions

D3’s transformation focus extends to animated transitions, too. Transitions interpolate styles and attributes over time. The time between can be controlled with easing functions.

D3’s interpolators support primitives (numbers and numbers within strings like font size, etc) and compound values. They are also extendable.

Examples:

To fade the background of the page to black:

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

Or, to resize circles in a symbol map with a staggered delay:

d3.selectAll("circle").transition() .duration(750) .delay(function(d, i) { return i * 10; }) .attr("r", function(d) { return Math.sqrt(d * scale); });

D3 allows you to modify only the elements that change, which reduces overhead and allows more complexity at high frame rates.

D3 allows sequencing of complex transitions via events

D3 does not replace the browser’s toolbox, but instead exposes it and makes it easier to use. You can still use CSS transitions.

Notes from Thinking with Joins by Mike Bostock

D3 has no primitive for creating multiple DOM elements. The .append() method can create single elements, but if you want multiple, you need to think in a different way.

Instead of telling D3 how to do something, tell D3 what you want. For example, on the Thinking with Joins page, Mike uses this snippet to tell D3 that the selection “circle” should correspond to data points. This concept is called a data join:

svg.selectAll("circle") .data(data) .enter().append("circle") .attr("cx", function(d) { return d.x; }) .attr("cy", function(d) { return d.y; }) .attr("r", 2.5);

Data points joined to the existing elements produce the update (inner) selection. Leftover unbound data produce the enter (left) selection, which represents missing elements. Any remaining unbound elements produce the exit (right) selection, which represents elements to be removed.

Now let’s explain the svg.selectAll("circle"):

First it returns an empty selection since the SVG container was empty. The parent node of the selection was the SVG container.

The selection is joined to a data array, resulting in three new selections that represent three possible states: enter, update, or exit. Since the selection was empty, the update and exit selections are empty and the enter selection contains a placeholder for each new data point.

The update selection is returned by selection.data and the enter and exit selections hang off the update selection. selection.enter returns the enter selection.

The missing elements are added to the SVG container by calling selection.append on the enter selection. This appends a new circle for each data point in the SVG container.

Thinking with joins means declaring a relationship between a selection (such as a “circle”) and data, then implementing this relationship through the three enter, update, and exit states.

For static visualizations, the enter selection is sufficient. But you can support dynamic visualizations with minor modifications to update and exit.

If a given enter, update, or exit selection happens to be empty, the corresponding code does not operate.

Joins let you target operations to specific states. For example, you can set constant attributes (such as the circle’s radius, defined by the “r” attribute) on enter rather than update. By reselecting elements and minimizing DOM changes, you vastly improve rendering performance!

Similarly, it allows you to target animations to specific states like expanding circles as the come in and contract circles as they go out.