PurplePanda
PurplePanda

Reputation: 673

How to structure nested nodes that need updating in d3 v4 force layout?

I am using d3 v4 and would like to do the nested selections like in this jsfiddle (but in a force layout with circles not text in a table): https://jsfiddle.net/nwozjscs/2/

I'm not sure where to start. My data looks like this [["circle1", "circle2"],["circle3", "circle4"],["circle5", "circle6"],["circle7", "circle8"]]. I would like to have a circle for every mini array and then append the circles inside that just above that circle in the force layout. The data is changing so I need the exit enter merge cycle for both the big data set of mini arrays and for each mini array. Are there any examples of nested selection in d3 v4 force layout? Where should I start with this?

Here is what I currently have however this doesn't append the small red circles at all and the updating doesn't seem to work.

// Create circle nodes
this.circleNode = this.d3Graph.selectAll("circle")

// Call our restartD3 function
this.restartD3();


// Function that restarts D3 and replace nodes with new this.users array (needed in when  add or remove a node)
restartD3() {

  // Circles
this.circleNode = this.circleNode.data(this.users);

 this.circleNode.exit().remove();

//maybe append a group here not circles
  this.rowenter = this.circleNode
  .enter()
    .append("circle")
    .attr("class", "circlenodes")
    .attr("r", 50)
    .attr("fill", "green")



  this.cell = this.circleNode.merge(this.rowenter)
  .selectAll(".circlenodes").data(function(d){
    return d.m;});

     this.cell.exit().remove();

    this.cell.enter().append("circle")
    .attr("class", "smallnodes")
    .attr("r", function(d){
      return 10
  })
  .attr("fill", "red")


  this.force.nodes(this.users);


}

Upvotes: 1

Views: 254

Answers (1)

Ian
Ian

Reputation: 34489

Update: A better solution using a proper nested join

So this got me thinking more - I didn't particularly like this approach in that the innerRender has to do lots of joins rather than a single big join. Felt like it could be improved. So it turns out that the selection.data function can also take a function which I hadn't realised before. (If you've not come across the page yet recommend reading https://bost.ocks.org/mike/nest/ ).

So I've modified my example - this is the render function. I've also added some transitions so visually it's easier to see what's going on.

render = (data) => {
const join = d3
    .select(".container")
    .selectAll("g")
    .data(data);

// Remove old groups
join
    .exit()
    .transition()
    .duration(500)
    .attr("transform", "scale(0)")
    .remove();

// Create the new groups
const groups = join
    .enter()
    .append("g");

// Add in the new circles
groups.append("circle")
    .attr("r", 0)
    .style("fill", "steelblue")
    .transition()
    .duration(500)
    .attr("r", RADIUS);

// Merge the new groups with the existing groups
// and apply an appropriate translation
const innerJoin = groups
    .merge(join)
    .attr("transform", d => `translate(${d.x},${d.y})`)
    .selectAll("circle.inner")
    .data(d => d);

// Remove old small circles
innerJoin
    .exit()
    .transition()
    .duration(500)
    .attr("r", 0);

 const newCircles = innerJoin
     .enter()
     .append("circle")
     .attr("class", "inner")
     .attr("r", 0)
     .style("fill", "orange")
     .attr("cy", -RADIUS - 5);

 newCircles
    .transition()
    .duration(500)
    .attr("r", 5);

 newCircles.merge(join)
     .attr("cx", (d, i) => 2 * i * 5);
}

See the full example JSFiddle


I recommend breaking this up into separate functions. Start with the regular d3.forceSimulation for the outer part - handling the join on the outer arrays and the forces being applied. Here's an example of how I'd do that render.

render = (data) => {
    const join = d3
        .select(".container")
        .selectAll("g")
        .data(data);

    // Remove old
    join.exit().remove();

    // Create the new groups
    const groups = join.enter().append("g");
    groups.append("circle")
        .attr("r", RADIUS)
        .style("fill", "steelblue");

    // Merge the new groups with the existing groups
    // and apply an appropriate translation
    groups.merge(join)
        .attr("transform", d => `translate(${d.x},${d.y})`)
        .each(function(d) {
            renderInner(this, d);
        });
}

Notice the call to renderInner? That's where I'd deal with the nesting, keeping it simple so actually you just treat it as another render...

renderInner = (element, data) => {
    const join = d3
        .select(element)
        .selectAll("circle.inner")
        .data(data);

     join.exit().remove();
     join.enter()
         .append("circle")
         .attr("r", 5)
         .style("fill", "orange")
         .attr("cy", -RADIUS - 5)
         .merge(join)
         .attr("cx", (d, i) => 2 * i * 5);
};

Here's an example that I put together to fully illustrate. If you wanted to it'd be really easy to map out those inner items to be fed into the nodes function on the forceSimulation so that they behave in the force too.

Here's a fully working example: JSFiddle

Upvotes: 1

Related Questions