Skip to main content

Dynamic and Interactive Content

Thus far we've looked at building static content, but the backbone of D3.js are it's beautiful transitions and dynamic updating capabilities.

Intervals

We need some way of repeatedly running code to change something the chart reacts to.  The easiest way to do this in d3 is using an interval.

const myInterval = d3.interval(() -> {
 	// code to loop
}, 500) //runs every 500ms

clearInterval(myInterval) // stop loop

The D3 Update Pattern

Let's debug the following code to observe how D3 objects work:

// JOIN data to create d3 virtual selectors
const rects = g.selectAll("rect")
      .data(data)

console.log(rects)

// EXIT - Old Elements
rects.exit().remove();

// ENTER - new elements onto the page
rects.enter().append("rect")
      .attr("y", d => y(d.revenue))
      .attr("x", (d) => x(d.month))
      .attr("width", x.bandwidth)
      .attr("height", d => HEIGHT - y(d.revenue))
      .attr("fill", "grey")

// UPDATE as needed
rects.attr("fill", "blue")

console.log(rects)

image.png

We now have an object with _enter, _exit, _groups and _parents. _enter contains elements in our data array that don't exist on the page (the shapes that need to be 'entered' on the screen). _exit is elements that are on the page but not in our data array (elements to be removed from the screen). _groups represents all elements that exist on the screen.

We always want to use the same update pattern when our data changes:

  1. Data Join - select all matching elements on the screen with selectAll() and update the data we're using
  2. Exit - Use the exit() selector to remove the elements that don't exist in our new array of data
  3. Update - set attributes for existing elements on screen
  4. Enter - use the enter() selector to set attributes for new items in our data array

Transitions

To give smooth updates, we can use the d3 transition suite. Which can be added with the following line:

rects.enter().append("rect")
      .attr("x", (d) => x(d.month))
      .attr("width", x.bandwidth)
      .attr("height", d => HEIGHT - y(d.revenue))
      .attr("fill", "grey")
      .transition(d3.transition().duration(500)) // gradually apply over 500ms
        .attr("y", d => y(d[value]))

Transitions are applied over time to each dynamic element of the graph. We can also define the transition as a variable so we don't have to write out d3.transition() over again. Always keep the transition length smaller than the loop's delay.

We can make the transitions even smoother by adding a exit transition and specifying the initial values before the transitions.

update(data) {
  ...
  
  const t = d3.transition().duration(500)
  
  rects.exit()
      .attr("fill", "red")
      .transition(t)
      .attr("height", 0)
      .attr("y", y(0))
      .remove()

  rects.transition(t)
      .attr("y", d => y(d[value]))
      .attr("x", (d) => x(d.month))
      .attr("width", x.bandwidth)
      .attr("width", x.bandwidth)
      .attr("height", d => HEIGHT - y(d[value]))

  rects.enter().append("rect")
      .attr("x", (d) => x(d.month))
      .attr("width", x.bandwidth)
      .attr("height", d => HEIGHT - y(d[value]))
      .attr("fill", "grey")
      .attr("fill-opacity", 1)
      .attr("y", y(0)) // Starting y axis
      .attr("height", 0) // Starting height
      .transition(t)
        .attr("y", d => y(d[value]))
        .attr("height", d => HEIGHT - y(d[value]))       
}

D3 Merge Method

You'll notice in the last code chunk the update and enter statement are somewhat repetitive. This can be resolved with the merge method.

  rects.exit()
      .attr("fill", "red")
      .transition(t)
      .attr("height", 0)
      .attr("y", y(0))
      .remove()

  rects.enter().append("rect")
      .attr("fill-opacity", 1)
      .attr("y", y(0))
      .attr("height", 0)
      // UPDATE is now part of transition
      .merge(rects)
      .transition(t) 
        .attr("width", x.bandwidth)
        .attr("fill", "grey")
        .attr("x", (d) => x(d.month))
        .attr("y", d => y(d[value]))
        .attr("height", d => HEIGHT - y(d[value]))

By default, d3 assumes the elements between data arrays stay in order. Let's say the first dataset is missing the first element, to resolve this we can add an argument to the data function to choose a key between different arrays.