Charts/d3Sync.js

"use strict";

const childNodesSelector = function() {
  return this.childNodes;
};

/**
 * Convenience wrapper that manages D3's enter/exit mechanics to synchronise an array of data
 * with DOM elements.
 * @param {Element} parent HTML element which will contain the nodes
 * @param {Object[]} arrayData Data to be synchronised.
 * @param {String} childElementTagName Name of HTML element to be created for each data point.
 * @param {Function} updateCallBack Function called with (d3object, isNewElement). If it returns the d3object,
 *      an opacity transition will be applied.
 * @param {Boolean} transition Parameter passed to d3.transition().
 */
function d3Sync(
  parent,
  arrayData,
  childElementTagName,
  updateCallback,
  transition = null
) {
  // move stray elements that are not 'childElementTagName' to the bottom
  // that way they can be removed with the fade out animation
  parent
    .selectAll(childNodesSelector)
    .filter((d, i, nodes) => nodes[i].tagName !== childElementTagName)
    .each(function() {
      this.parentElement.appendChild(this);
    });
  // synchronise intended elements
  const existing = parent
    .selectAll(childNodesSelector)
    .data(arrayData, function(d) {
      return d ? d.id || d : this.id;
    }); // id hack for ChartData objects which get re-created each time there are any changes
  const enter = existing.enter().append(childElementTagName);
  const exit = existing.exit();
  if (transition) {
    exit
      .transition(transition)
      .style("opacity", 1e-2)
      .remove();
    const entered = updateCallback(enter, true);
    if (entered) {
      // We don't want to randomly transition all attributes on new elements, because it looks bad.
      // Instead, let's just transition opacity.
      entered
        .style("opacity", 0)
        .transition(transition)
        .style("opacity", 1);
    }
    updateCallback(existing.transition(transition).style("opacity", 1), false);
  } else {
    exit.remove();
    updateCallback(enter, true);
    updateCallback(existing, false);
  }
}

module.exports = d3Sync;