1

I'm building an interactive graph with D3.js (v4) where each parent node has a collapsible set of child nodes. Because I also want to create tooltips etc, I'm wrapping each circle with g. Relevant part of my code looks like this:

var node = svg.append("g").selectAll(".node");

...

function update() {

    nodes = getVisibleNodes()

    ...

    node = node.data(nodes, function(d) { return d.name; });
    node.exit().remove();
    node = node.enter()
        .append("g")
        .classed("node", true)
        .merge(node);

    node
        .append("circle")
        .attr("r", function(d) { return d.size / 500; })
        .on("click", click)
        .on("contextmenu", rightclick)
        .call(drag);
}

At first everything seems to work fine, but then I noticed that every time I run update and some nodes are shown/hidden, new entering nodes are appended there on top of the old ones. If I click around for a while, I end up with tens of circles in each g, drawn one on top of another (it's easily visible when they're semi-transparent).

I don't really understand what's going on here, and what am I doing wrong. Any good advice for me?

machaerus
  • 749
  • 1
  • 6
  • 20

1 Answers1

2

Just skimming through your code we can see that you have the "update"/"enter"/"exit" selections for the groups, but not for the circles.

Therefore, when a group is neither in the "enter" nor in the "exit" selections, meaning that it is in the "update" selection, you're appending a new circle to it every time you run the update function.

Here is a very basic demo to show it. This is a wrong code: data here is a random array with 10 elements maximum. But, as you can see, sometimes the number of circles is way more than 10:

var svg = d3.select("svg");
d3.select("button").on("click", update);
var color = d3.scaleOrdinal(d3.schemeCategory20)

function update() {
  var data = d3.range(~~(Math.random() * 10)).map(function(d) {
    return {
      size: ~~(Math.random() * 20),
      x: ~~(Math.random() * 300),
      y: ~~(Math.random() * 150)
    }
  })
  var node = svg.selectAll(".node").data(data);
  node.exit().remove();
  node.enter()
    .append("g")
    .classed("node", true)
    .merge(node)
    .append("circle")
    .attr("fill", function(d) {
      return color(d.size)
    })
    .attr("r", function(d) {
      return d.size;
    })
    .attr("cx", function(d) {
      return d.x;
    })
    .attr("cy", function(d) {
      return d.y;
    })
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<button>Update</button>
<br>
<svg></svg>

Solution:

Don't use merge, separate your enter and update selections for the groups. And, in the update selection, select existing circles.

node.select("circle")
    .attr("r", etc...

Here is the demo of a correct code, it never has more than 10 circles:

var svg = d3.select("svg");
d3.select("button").on("click", update);
var color = d3.scaleOrdinal(d3.schemeCategory20)

function update() {
  var data = d3.range(~~(Math.random() * 10)).map(function(d) {
    return {
      size: ~~(Math.random() * 20),
      x: ~~(Math.random() * 300),
      y: ~~(Math.random() * 150)
    }
  })
  var node = svg.selectAll(".node").data(data);
  node.exit().remove();
  node.enter()
    .append("g")
    .classed("node", true)
    .append("circle")
    .attr("fill", function(d) {
      return color(d.size)
    })
    .attr("r", function(d) {
      return d.size;
    })
    .attr("cx", function(d) {
      return d.x;
    })
    .attr("cy", function(d) {
      return d.y;
    })

  node.select("circle")
    .transition()
    .duration(1000)
    .attr("fill", function(d) {
      return color(d.size)
    })
    .attr("r", function(d) {
      return d.size;
    })
    .attr("cx", function(d) {
      return d.x;
    })
    .attr("cy", function(d) {
      return d.y;
    })
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<button>Update</button>
<br>
<svg></svg>
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
  • It doesn't seem to work for me, maybe because in your solution you don't re-assign the `node` variable. (?) Because I use force simulation, I use `node` also in a separate `tick()` method, like: `node.attr("transform", function(d) {...})`, and when I tried doing what you suggested, the position of nodes doesn't update at all (they're all in the upper left corner). – machaerus Jul 18 '17 at 13:37
  • 1
    It's really hard to provide an answer without a [MCVE]. However, have in mind that my answer does address the problem stated in the title of your question, and shows how to fix it. If you have another issues, I suggest you post another question, or edit this question clarifying exactly what is your problem and creating a [MCVE]. – Gerardo Furtado Jul 18 '17 at 13:44
  • Ok, nevermind, just solved this issue. Thanks, your answer was useful! – machaerus Jul 18 '17 at 15:29