Home > Software design >  D3 force directed graph nodes with nested circles
D3 force directed graph nodes with nested circles

Time:04-11

Within my force directed graph, I wish that each of my nodes have :

  • Two circles.
  • Both circles must have the same size.
  • The circles need to be on the top of each other so that visually we only see one.
  • The circle that is beneath is the circle that has the red filling.
  • The circle on top is the one that has the url as filling.

What am I doing wrong ?

    const node = svg.append("g")
        .attr("stroke", nodeStroke)
        .attr("stroke-opacity", nodeStrokeOpacity)
        .attr("stroke-width", nodeStrokeWidth)

        .selectAll("circle")
        .data(nodes)
        .join("circle")
        .style("fill", "red")
        .attr("r", 30)
        
        node.selectAll("circle")
        .data(nodes)
        .join("circle")
        .style("fill", d => `url(#${d.id})`)
        .attr("r", 30)
        .call(drag(simulation))
        

I have tried to apply @Andrew Reid's tips but probable did it wrong. The thing is that the parent circle and the nested circle both use the same data.

  const node = svg.append("g")
        .attr("stroke", nodeStroke)
        .attr("stroke-opacity", nodeStrokeOpacity)
        .attr("stroke-width", nodeStrokeWidth)
        
        .selectAll(".circleB")
        .data(nodes)
        .join("circle").attr("class","circleB")
        .style("fill", "red")
        .attr("r", 20)
 
        .join("circle").attr("class","circleC")
        .style("fill", d => `url(#${d.id})`)
        .attr("r",10)

CodePudding user response:

This is the expected behavior:

node is a selection of circles - your code block here:

  svg.append("g")
    .attr("stroke", nodeStroke)
    .attr("stroke-opacity", nodeStrokeOpacity)
    .attr("stroke-width", nodeStrokeWidth)
    .selectAll("circle")
    .data(nodes)
    .join("circle")
    .style("fill", "red")
    .attr("r", 30)

returns a selection of circles ultimately, which is what nodes refers to. The second selectAll statement attempts to append a circles to this selection of circles. A circle SVG element cannot contain a child circle element as this is invalid syntax, so you none of these child circles will render.

If we break that chain up, we can get a selection of a parent g to append both sets of circles:

 const g = svg.append("g")
    .attr("stroke", nodeStroke)
    .attr("stroke-opacity", nodeStrokeOpacity)
    .attr("stroke-width", nodeStrokeWidth)

Then we can use g.selectAll() to ensure we append circles to a legal parent, the g. However, if we don't adjust your code a bit, you'll only get one set of circles as the second selectAll() statement will select all the circles added following the first selectAll() statement: The first time you do this you have no circles, all circles are entered/appended. The second time you do this you have circles, one for every item in your data array, so no new circles are appended.

You can apply class names to differentiate in the selectAll statements:

 .selectAll(".circleA")
 .data(...)
 .join("circle")
 .attr("class", "circleA")

However, if you never intend to add the circles once and leave their data unmodified, you can use .selectAll("null").

CodePudding user response:

As Andrew Reid alluded to, since you want to add multiple circles to the same node, you probably want to do this with one svg group element per node (g elements). From there you can add multiple circles by repeatedly taking the same group selection and appending to it.

Without Updates

For instance, here's a simple example that uses prepopulated images, courtesy of the Wikimedia Commons. It works in cases where you set the data only once:

var nodeStroke = 'black';
var nodeStrokeWidth = 5;
var nodeStrokeOpacity = 0.8;
var nodes = [{id: 'image-1' }, {id: 'image-2' }];

const svg = d3.select('svg')
        .attr("stroke", nodeStroke)
        .attr("stroke-opacity", nodeStrokeOpacity)
        .attr("stroke-width", nodeStrokeWidth);     
var group = svg
        .selectAll()
        .data(nodes)
        .join("g")
        .attr("class", "node")
        .attr("transform", (d,i)=>`translate(${i * 100   50},100)`);
var background = group.append("circle")
        .attr("class", "background") // classes aren't necessary here, but they can help with selections/styling
        .style("fill", "red")
        .attr("r", 30)
var foreground = group.append("circle")
        .attr("class", "foreground") // classes aren't necessary here, but they can help with selections/styling
        .style("fill", d => `url(#${d.id})`)
        .attr("r", 30)
// can also start drag simulation here:
// group.call(drag(simulation));
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg>
  <defs>
    <pattern id="image-1" x="30" y="30" patternUnits="userSpaceOnUse" height="60" width="60">
      <image x="-44" y="-30" xlink:href="https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Mona_Lisa.jpg/158px-Mona_Lisa.jpg"></image>
    </pattern>
    <pattern id="image-2" x="30" y="30" patternUnits="userSpaceOnUse" height="60" width="60">
      <image x="-40" y="-30" xlink:href="https://upload.wikimedia.org/wikipedia/commons/thumb/9/90/Monet_w1709.jpg/163px-Monet_w1709.jpg"></image>
    </pattern>
  </defs>
</svg>

With updates

When updating data, you want to only add circles to new elements. Here we can move the circle appends to be in the first function argument of join (the enter function).

var nodeStroke = 'black';
var nodeStrokeWidth = 5;
var nodeStrokeOpacity = 0.8;

// if using a drag simulation, may need to have only one instance of it:
// var dragSimulation = drag(simulation);

const svg = d3.select('svg')
        .attr("stroke", nodeStroke)
        .attr("stroke-opacity", nodeStrokeOpacity)
        .attr("stroke-width", nodeStrokeWidth);
function updateData(nodes) {
  var group = svg
        .selectAll(".circle-group")
        .data(nodes)
        .join(enter => {
          var newNodes = enter.append("g")        
            .attr("class", "circle-group");
          newNodes.append("circle")
            .attr("class", "background") // classes aren't necessary here, but they can help with selections/styling
            .style("fill", "red")
            .attr("r", 30);
          newNodes.append("circle")
            .attr("class", "foreground") // classes aren't necessary here, but they can help with selections/styling
            .style("fill", d => `url(#${d.id})`)
            .attr("r", 30)
          // if using the drag simulation:
          // newNodes.call(dragSimulation);
          return newNodes;
        })
        .attr("transform", (d,i)=>`translate(${i * 100   50},100)`);
}
var data =  [{id: 'image-1' }, {id: 'image-2' }];
updateData(data);
setTimeout(function () {
  data.push({id: 'image-1' });
  updateData(data);
}, 2000);
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg>
  <defs>
    <pattern id="image-1" x="30" y="30" patternUnits="userSpaceOnUse" height="60" width="60">
      <image x="-44" y="-30" xlink:href="https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/Mona_Lisa.jpg/158px-Mona_Lisa.jpg"></image>
    </pattern>
    <pattern id="image-2" x="30" y="30" patternUnits="userSpaceOnUse" height="60" width="60">
      <image x="-40" y="-30" xlink:href="https://upload.wikimedia.org/wikipedia/commons/thumb/9/90/Monet_w1709.jpg/163px-Monet_w1709.jpg"></image>
    </pattern>
  </defs>
</svg>

  • Related