Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3 V4 Transition on entering elements using General Update Pattern with merge

To the best of my understanding it is not possible to include a transition on the entering elements in the standard enter/append/merge chain, since doing so would replace the entering element selection with a transition that cannot be merged with the update selection. (See here on the distinction between selections and transitions).

(Question edited in response to comment)

If the desired effect is sequenced transitions, one before and one after the merge, it can be accomplished as follows:

// Join data, store update selection      
      circ = svg.selectAll("circle")
          .data(dataset);

// Add new circle and store entering circle selection        
      var newcirc = circ.enter().append("circle")
         *attributes*

// Entering circle transition        
      newcirc    
          .transition()
          .duration(1000)
          *modify attributes*
          .on("end", function () {

// Merge entering circle with existing circles, transition all        
      circ = newcirc.merge(circ)
          .transition()
          .duration(1000)
          *modify attributes*
      });

jsfiddle

I would like to know if there is a way to do this without breaking up the enter/append/merge chain.

like image 768
jtr13 Avatar asked Apr 07 '18 13:04

jtr13


1 Answers

There can be no doubt, that you have to have at least one break in your method chaining since you need to keep a reference to the update selection to be able to merge it into the enter selection later on. If you are fine with that, there is a way to keep the chain intact after that initial break.

I laid out the basic principal for this to work in my answer to "Can you chain a function after a transition without it being a part of the transition?". This uses transition.selection() which allows you to break free from the current transition and get access to the underlying selection the transition was started on. Your code is more complicated, though, as the chained transition adds to the complexity.

The first part is to store the update selection like you did before:

// Join data, store update selection      
const circUpd = svg.selectAll("circle")
  .data(dataset);

The second, uninterrupted part goes like this:

const circ = circUpd              // 2. Store merged selection from 1.
  .enter().append("circle")
    // .attr()...
  .transition()
    // .duration(1000)
    // .attr()...
    .on("end", function () {
      circ.transition()           // 3. Use merged selection from 2.
        // .duration(1000)
        // .attr()...
    })
    .selection().merge(circUpd);  // 1. Merge stored update into enter selection.

This might need some further explanations for the numbered steps above:

  1. The last line is the most important one—after kicking off the transition, the code uses .selection() to get a hold of the selection the transition was based on, i.e. the enter selection, which in turn can be used to easily merge the stored update selection into it.

  2. The merged selection comprising both the enter and the update selection is the result of the entire chain and is then stored in circ.

  3. This is the tricky part! It is important to understand, that the function provided to .on("end", function() {...}) is a callback which is not executed before the transition ends. Although this line comes before the merging of the selections, it is actually executed after that merge. By referring to circ, however, it closes over—captures, if you will—the reference to circ. That way, when the callback is actually executed, circ will already refer to the previously merged selection.

Have a look at the following working snippet:

var w = 250;
var h = 250;
          
// Create SVG          
var svg = d3.select("body")
  .append("svg")
    .attr("width", w)
    .attr("height", h);

// Create background rectangle          
svg.append("rect")
  .attr("x", "0")
  .attr("y", "0")
  .attr("width", w)
  .attr("height", h)
  .attr("fill", "aliceblue");

var dataset = [170, 220, 40, 120, 0, 300];

var xScale = d3.scaleBand()
  .domain(d3.range(dataset.length))
  .range([0, w]);
      
var yScale = d3.scaleLinear()
  .domain([0, 300])
  .range([75, 200])
  
var rad = xScale.bandwidth()/2
  
// Join data  
var circ = svg.selectAll("circle")
  .data(dataset);

// Create initial circles
circ.enter().append("circle")
    .attr("cx", (d, i) => xScale(i)+rad)
    .attr("cy", d => yScale(d))
    .attr("r", rad)
    .attr("fill", "blue");

// Trigger update on click
d3.select("h3")
  .on("click", function () {

    // Create new data value        
    var newvalue = Math.floor(Math.random()*300);
        
    dataset.push(newvalue);

    xScale.domain(d3.range(dataset.length));
        
    rad = xScale.bandwidth()/2;
 
    // Join data, store update selection      
    const circUpd = svg.selectAll("circle")
      .data(dataset);
    
    // Add new circle and store entering circle selection        
    const circ = circUpd              // 2. Store merged selection from 1.
      .enter().append("circle")
        .attr("cx", "0")
        .attr("cy", "25")
        .attr("r", rad)
        .attr("fill", "red")
      .transition()
        .duration(1000)
        .attr("cx", (d, i) => xScale(i)+rad)
        .on("end", function () {
          circ.transition()           // 3. Use merged selection from 2.
              .duration(1000)
              .attr("cx", (d, i) => xScale(i)+rad)
              .attr("cy", d => yScale(d))
              .attr("r", rad)
              .attr("fill", "blue");
  	    })
      .selection().merge(circUpd);  // 1. Merge stored update into enter selection.

});
<script src="https://d3js.org/d3.v4.js"></script>
<h3>Add a circle</h3>
like image 162
altocumulus Avatar answered Nov 14 '22 23:11

altocumulus