Writing idempotent D3

Some parts of D3 are naturally idempotent. For example, data joins using the General Update Pattern:

const points = d3.selectAll('.datapoint')
  .data([ /* ... */]);

points.enter().append('div')
  .attr('class', 'datapoint')
  .text(d => d);

points.exit().remove();

If that code were wrapped in a reusable chart function, we could call it multiple times and get the same resulting DOM.

drawChart([ 'lonely div' ]);
drawChart([ 'lonely div' ]);

// RESULT ✅
// <div class="datapoint">lonely div</div>

Non-data-bound append operations, however, are not idempotent.

d3.select('body').append('svg');

This code will not render the same DOM when called multiple times.

drawChart();
drawChart();

// RESULT ❌
// <svg></svg><svg></svg>

Non-data-bound idempotence

For our chart components to be useful components, we have to make both data-bound and non-data-bound render idempotently.

For non-data-bound, we have to do our own DOM diffing to test whether an element has already been created.

let svg = d3.select('body').select('svg.my-chart');

if (svg.empty()) {
  svg = d3.select('body').append('svg').attr('class', 'my-chart');
}

svg.selectAll('.datapoints')
  .data([ /* ... */ ]);

// etc.

That works, but instead of writing this redundant pattern for every element, we can extend d3's selection function with a shortcut.

At POLITICO, we use a little function called appendSelect:

/**
 * appendSelect either selects a child of current selection or appends
 * one to the selection if it doesn't exist. Useful for writing idempotent
 * chart functions.
 *
 * Used like this:
 *
 * selection.appendSelect('div');
 * selection.appendSelect('div', 'with-a-class');
 * selection.appendSelect('div', 'one-class two-classes');
 * selection.appendSelect('div').appendSelect('div', 'nested');
 *
 * @param  {string} elementToAppend   String representation of element to be appended/selected.
 * @param  {String} elementClass      Class string (w/out dots) of element to be appended/
 *                                    selected. Can pass none or multiple separated by whitespace.
 * @return {object}                   d3 selection of child element
 */
d3.selection.prototype.appendSelect = function (elementToAppend, elementClass) {
  const selector = elementClass ?
    `${elementToAppend}.${elementClass.split(' ').join('.')}` :
    elementToAppend;

  const selected = this.select(selector);

  // If element doesn't already exist, append it...
  if (selected.empty()) {
    return elementClass ?
      this.append(elementToAppend).classed(elementClass, true) :
      this.append(elementToAppend);
  }
  // ... if it does, return it.
  return selected;
};

Here's how we use it:

d3.select('#chart')
  .appendSelect('h1', 'title')
  .text('My chart');


// Can be chained to create nested elements
d3.select('#chart')
  .appendSelect('svg')
  .appendSelect('g', 'chart group');

// RESULT ✅
// <div id="chart">
//   <h1>My chart</h1>
//   <svg>
//     <g class="chart group"></g>
//   </svg>
// </div>