Advanced Rendering with Callbacks

Overview

By default, D3 visualizations created with r2d3 are monolithic scripts that do all of their work as a single computation (the body of the script). While this makes scripts straightforward to author by default, not all visualizations will confirm to this simple model. Rather, some visualizations will want to distinguish between the following activities:

  1. Code that performs one-time initialization for the visualization (e.g. creation of a D3 layout).

  2. Code that runs whenever the data underlying the visualization is changed.

  3. Code that runs whenever the visualizations container element is resized.

This article describes how you can use the r2d3 advanced rendering API to organize your visualization code to run in this more granular fashion.

onRender Callback

To distinguish between code that runs at initialization-time only and code that runs when data changes, organize your code so that the code which responds to data changes is contained within the r2d3.onRender() callback. For example:

// Initialization
svg
 .attr("font-family", "sans-serif")
 .attr("font-size", "8")
 .attr("text-anchor", "middle");
    
var pack = d3.pack()
  .size([width, height])
  .padding(1.5);
    
var format = d3.format(",d");
var color = d3.scaleOrdinal(d3.schemeCategory20c);

// Rendering
r2d3.onRender(function(data, svg, width, height, options) {
  var root = d3.hierarchy({children: data})
    .sum(function(d) { return d.value; })
    .each(function(d) {
      if (id = d.data.id) {
        var id, i = id.lastIndexOf(".");
        d.id = id;
        d.package = id.slice(0, i);
        d.class = id.slice(i + 1);
      }
    });

  var node = svg.selectAll(".node")
    .data(pack(root).leaves())
    .enter().append("g")
      .attr("class", "node")
      .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

  node.append("circle")
      .attr("id", function(d) { return d.id; })
      .attr("r", function(d) { return d.r; })
      .style("fill", function(d) { return color(d.package); });

  node.append("clipPath")
      .attr("id", function(d) { return "clip-" + d.id; })
    .append("use")
      .attr("xlink:href", function(d) { return "#" + d.id; });

  node.append("text")
      .attr("clip-path", function(d) { return "url(#clip-" + d.id + ")"; })
    .selectAll("tspan")
    .data(function(d) { return d.class.split(/(?=[A-Z][^A-Z])/g); })
    .enter().append("tspan")
      .attr("x", 0)
      .attr("y", function(d, i, nodes) { return 13 + (i - nodes.length / 2 - 0.5) * 10; })
      .text(function(d) { return d; });

  node.append("title")
      .text(function(d) { return d.id + "\n" + format(d.value); });
});

onResize Callback

By default, when the element which contains your visualization is resized, r2d3 will call back your script to re-render the visualization from scratch using the new size. In some cases this might be acceptable, but in other cases it makes sense to have code which explicitly handles the resize in a more efficient fashion.

You can provide an explicit resize handler by implementing the r2d3.onResize() callback. For example, in a force directed D3 layout you might do this in the onResize() callback:

r2d3.onResize(function(width, height) {
  force.force("center", d3.forceCenter(width / 2, height / 2))
    .restart();
});