javascriptangulard3.jsd3-force-directed

Duplicate/nested nodes on d3 for any new data updating force directive graph


Here is a demo

When new data hits my d3 service it loads the new data set but the old isn't removed. Therefore I have duplicate nodes inside its parent node 'g' element. New to d3, however I've done lots of reading around selection.join() instead of enter().append(). I've also read up on ways to add node.exit().remove(); and node.merge(node); at specific points.

As you can see from the dom, all new node properties are in the <g class="node"> element, duplicated, not replacing the original data. Therefore I get a overlapping of content.

enter image description here

Here is the way my nodes are built...

const zoomContainer = d3.select('svg g');

const node = zoomContainer.selectAll('g').data(nodes, function (d) {
  return d.id;
});

//zoomContainer.selectAll('.node').data(node).exit().remove();

const nodeEnter = node
  .join('g')
  .attr('class', 'node')
  .call(
    d3
      .drag()
      .on('start', (d) => this.dragended(d3, d, simulation))
      .on('drag', function dragged(d) {
        d.fx = d3.event.x;
        d.fy = d3.event.y;
      })
      .on('end', (d) => this.dragended(d3, d, simulation))
  );

nodeEnter
  .append('circle')
  .style('fill', '#fff')
  .style('cursor', 'pointer')
  .style('fill-opacity', '1')
  .style('stroke-opacity', '0.5')
  .attr('id', (d, i) => d.id)
  .attr('r', 28);

nodeEnter
  .append('image')
  .attr('xlink:href', 'https://github.com/favicon.ico')
  .attr('x', -15)
  .attr('y', -60)
  .attr('width', 16)
  .attr('class', 'image')
  .style('cursor', 'pointer')
  .attr('height', 16);

const nodeText = nodeEnter
  .data(nodes)
  .append('text')
  .style('text-anchor', 'middle')
  .style('cursor', 'pointer')
  .attr('dy', -3)
  .attr('y', -25)
  .attr('class', 'nodeText')
  .attr('id', 'nodeText');

nodeText
  .selectAll('tspan')
  .data((d, i) => d.label)
  .join('tspan')
  .attr('class', 'nodeTextTspan')
  .text((d) => d)
  .style('font-size', '12px')
  .attr('x', -10)
  .attr('dx', 10)
  .attr('dy', 15);

I probably could clear the graph by force but I like and need the way .join() can compare what's changed and the options to use enter().append().exit(). If anybody can see why duplicates are not being removed/merged I would appreciate it.

UPDATE:

If I use enter().append('g') instead of join('g') I then get a better result. I can use zoomContainer.selectAll('.node').data(node).exit().remove(); before hand and my nodes do get updated but only after clicking update twice. If I use join('g') they duplicate and I am unable to use zoomContainer.selectAll('.node').data(node).exit().remove();

Here is a demo


Solution

  • Targeting the <g> element rather than the class and then using exit().remove() seemed to have done the trick... I was adding a class attribute at the .enter() level in .join() and then doing the exit on that. Demo here

    const node = zoomContainer
      .selectAll('.node')
      .data(this.nodes, function (d) {
        return d.id;
      });
    zoomContainer.selectAll('g').data(node).exit().remove();