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:
// JoiningJOIN data to create d3 virtual selectors
const rects = g.selectAll("rect")
.data(data)
console.log(rects)
// RemoveEXIT - Old Elements
rects.exit().remove();
// EnteringENTER - 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")
// UpdateUPDATE as needed
rects.attr("fill", "blue")
console.log(rects)
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:
- Data Join - select all matching elements on the screen with selectAll() and update the data we're using
- Exit - Use the exit() selector to remove the elements that don't exist in our new array of data
- Update - set attributes for existing elements on screen
- 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.