Using form elements to update D3 visualizations

Preview image

A common requirement is to have form elements on the page that can alter the data being displayed by your prized visualization.

Dropdowns, checkboxes, and input fields can all be leveraged to add filtering, sorting, or any number of behaviors to your previously one-trick-pony D3 project. While every project will have unique details, the high level parts of this problem are fairly consistent between implementations.

To build a dynamic D3 visualization that interacts with external form elements, you must detect changes to the form, communicate changes to the form, and act on changes to the form.

But first, Step 0

Before we can do all those things in response to changes in our form, we have to, you know, build our form.

How your form gets build is one of those infinite details, so it's a good thing D3 doesn't much care how it happens. You can lovingly hand-code HTML, you can use a form library for Angular, or React, or whatever framework you're using. You can even use D3 to create your form if you want.

d3.select('#target').append('select') and off you go. 😄

The method you choose should be dictated by the context in which the form will exist, if the form itself is dynamic or static, and probably at least 17 more factors. Moving right along...

Detecting changes to the form

The way you detect changes will depend on the way your form is created, but for our purposes we're going to use a statically defined form and listen to it with vanilla JavaScript.

Our markup looks like this, with a chart placeholder and a simple select element.

<div id="chart"></div>
 
<select name="subjects" onchange="onSubjectChange()">
  <option value="math">Math</option>
  <option value="science">Science</option>
  <option value="language">Language</option>
</select>

Our select element has an onchange handler defined directly in the markup, which maps to this function.

function onSubjectChange() {
  const selector = 'select[name="subjects"]'
  const subject = document.querySelector(selector).value
  render(subject) // communicate the change
}

As you can see, our change handler gets the newly selected subject and then passes it into the render function. We'll see that function soon enough, but it does exactly wht you'd expect.

Communicating changes to the form

Since our example is small and contrived, communicating the change is just a matter of calling a function. If changes to the form don't need to be persisted or sent to a server, you can probably use a simple function call as well.

If your visualization is part of a larger app, or you want to remember the user's customized view, you'll likely need to create/update a cookie, send the new values to the server, or perform any number of other steps. Essentially, anything that is not updating the visualization itself can be seen as falling under this step in the process.

Acting on changes to the form

With the changes applied (if necessary) we can get down to actually updating our visualization. To keep this tip short we won't review all the code used to initially create the column chart. To review that code see the full CodePen example. Instead we'll only review the render function that gets called whenever a new subject is selected from our dropdown.

function render (subject = 'math') {
  const update = svg.selectAll('rect')
    .data(data.filter(d => d[subject]), d => d.name);
 
  update.exit()
    .remove();
 
  update
    .transition()
    .attr('y', d => yScale(d[subject]))
    .attr('height', d => height - yScale(d[subject]));
 
  update
    .enter()
    .append('rect')
    .style('fill', 'steelblue')
    .attr('x', d => xScale(d.name))
    .attr('width', d => xScale.bandwidth())
    .attr('y', d => yScale(d[subject]))
    .attr('height', d => height - yScale(d[subject]));
}

This is all pretty standard D3 code, but notice how many times the subject parameter is used. It's used to filter our data and to set our y and height attributes.

One thing to note is that seeing d[subject] repeated in the function will likely trigger a reflexive thought that we should create a shortcut to it at the top of the function. Look a bit closer, however, and you'll notice that all of those uses are within anonymous arrow functions where d is the datum from our dataset being passed into the function so we can calculate the proper corresponding value.

The other important takeaway is that this function can be called from anywhere. It would have been easy to use render directly as our change handler, but then it would have to concern itself with getting the actual subject value from the DOM event. Instead, we've created a "pure" function that accepts a subject and updates the visualization accordingly. Thanks to that decision it's easy to call render() at startup to trigger the initial render using the default subject of math.

That's it!

I hope this has been helpful in clearing up where some of the boundaries can be drawn when integrating D3 with the outside world. It's impossible to cover everyone's unique scenario, but hopefully these higher level, abstract phases at least give you an idea of how to tackle something like this in your own app.

Happy hacking!