Gabriel Sánchez
Gabriel Sánchez

Reputation: 21

Exiting unused nodes in d3 force directed graph

I am trying to create a force directed graph using d3. I got it working following this example for the enter case, so loading the code for the first time. But when I tried to incorporate some interactivity by (in this case) clicking a node, I noticed that the old nodes where not exiting correctly. I've looked at other examples and even questions like this one but still am unable to decypher what is wrong. Thanks in advance.

Index.js:

let globaldata = null;
let graphSvg = d3.select('svg');
let directedGraph = null;
let selectedNode = "construct";

function render() {
  let data = globaldata;

  //Directed Graph
  graphSvg
    .attr("height", 500)
    .attr("width", 750);
  let graphMargin = {
    top: 50,
    right: 100,
    bottom: 100,
    left: 80
  };
  let graphNodes = [];
  let graphLinks = [];

  let linkData = d3.nest()
    .key(function(d) {
      return d.callerType;
    })
    .key(function(d) {
      return d.taskType;
    }).entries(data);
  linkData.shift();
  linkData.forEach(element => {
    let node = {
      id: element.key
    };
    graphNodes.push(node);
  });
  console.log(JSON.parse(JSON.stringify(graphNodes)));
  linkData.forEach(element => {


    let sourceK = element.key;
    let targetK = element.values[0].key;
    graphLinks.push({
      source: graphNodes.find(sourceN => sourceN.id == sourceK),
      target: graphNodes.find(targetN => targetN.id == targetK),
      amount: element.values[0].values.length
    });

  });
  console.log(linkData);
  console.log(graphLinks);
  if (!directedGraph) {
    directedGraph = new DirectedGraph(graphSvg, graphNodes, graphLinks, nodeClick, graphMargin);
  } else {
    directedGraph.render(graphNodes, graphLinks);
  }
};

function nodeClick(id) {
  selectedNode = id;
  console.log("Selected Node:")
  console.log(selectedNode)
  render();
}


d3.csv('data/daten.csv', function(d) {
  return {
    taskID: d['TaskID'],
    taskType: d['TaskType'],
    start: +d['Start'],
    end: +d['End'],
    caller: d['Caller'],
    callerType: d['CallerType'],
    gen: +d['Generation']
  };
}).then(function(data) {
  globaldata = data
  render();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

<svg />

And the directedGraph module:

DirectedGraph = function(svg, nodes, links, clickFunc, margin) {
  this.svg = svg;
  this.margin = margin;
  this.height = +svg.attr("height");
  this.width = +svg.attr("width");
  this.innerHeight = this.height - this.margin.top - this.margin.bottom;
  this.innerWidth = this.width - this.margin.left - this.margin.right;
  this.xCenter = this.innerWidth * 0.5;
  this.yCenter = this.innerHeight * 0.5;
  this.clickFunc = clickFunc;


  this.transformG = this.svg.append("g")
    .attr("transform", `translate(${this.margin.left},${this.margin.top})`);


  //Build arrowhead
  this.transformG.append("defs").append("marker")
    .attrs({
      "id": "arrowhead",
      "viewBox": "-4 -5 10 10",
      "refX": 13,
      "refY": 0,
      "orient": "auto-start-reverse",
      "markerWidth": 11,
      "markerHeight": 7,
      "xoverflow": "visible"
    })
    .append("svg:path")
    .attr("d", "M -4,-5 L 6 ,0 L -4,5")
    .attr("fill", "#999")
    .style("stroke", "none");


  this.links = this.transformG.append("g").attr("class", "links").selectAll(".link")

  this.nodesG = this.transformG.append("g").attr("class", "nodes").selectAll(".node");

  this.render(nodes, links);

};
DirectedGraph.prototype.render = function(nodes, links) {
  let vis = this;
  //ColorScale for node Color
  let taskColor = d3.scaleOrdinal(d3.schemePastel1);

  let amount = 0;
  links.forEach(element => {
    amount += element.amount;
  })
  let stroke = d3.scaleLinear()
    .domain([1, amount])
    .range([0.3, 20]);

  //Radius of the Nodes
  let nodeRadius = 10;


  //Setting initial position of construct node
  let pos0Node = nodes.filter(n => n.id === "construct");
  pos0Node.fx = this.xCenter;
  pos0Node.fy = this.yCenter;


  //force direction Layout Simulation
  simulation = d3.forceSimulation()
    .force("link", d3.forceLink().distance(60).id(function(d) {
      return d.id;
    })) // .strength(0.2)
    .force("charge", d3.forceManyBody().distanceMin(20).distanceMax(100).strength(-100))
    .force("center", d3.forceCenter(this.xCenter, this.yCenter));



  this.links = this.links.data(links);

  this.links.exit().remove();

  let gLinkEnter = this.links.enter().append("svg:path").attr("class", "link");

  gLinkEnter
    .attr("stroke", "#999999")
    .attr("marker-end", "url(#arrowhead)")
    .merge(this.links)
    .style("stroke-width", function(d) {
      return Math.sqrt(stroke(d.amount))
    })
    .attrs({
      "class": "edgelabel1",
      "id": function(d, i) {
        return "edgelabel1" + i
      },
      "font-size": 10,
      "fill": "none",

    });

  this.nodesG = this.nodesG.data(nodes);


  this.nodesG.exit().remove();

  let gNodeEnter = this.nodesG.enter().append("circle").attr("class", "node");

  gNodeEnter
    .merge(this.nodesG)
    .attr("r", nodeRadius)
    .attr("fill", function(d, i) {
      return taskColor(i);
    })
    .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended)
    )
    .on('click', d => this.clickFunc(d.id));


  simulation
    .nodes(nodes)
    .on("tick", ticked());
  simulation.force("link")
    .links(links);


  function dragstarted(event, d) {
    if (!event.active) simulation.alphaTarget(0.3).restart()
    d.fx = d.x;
    d.fy = d.y;
  }

  function dragged(event, d) {
    d.fx = event.x;
    d.fy = event.y;
  }

  function dragended(event, d) {
    if (!event.active) simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
  }

  function ticked() {
    /*gLinkEnter
        .attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });
    */
    gLinkEnter.attr("d", function(d) {
      let x1 = d.source.x;
      let y1 = d.source.y;
      let x2 = d.target.x;
      let y2 = d.target.y;
      let dx = x2 - x1;
      let dy = y2 - y1;
      let dr = Math.sqrt(dx * dx + dy * dy);

      // Defaults for normal edge.
      let drx = 0;
      let dry = 0;
      let xRotation = 0; // degrees
      let largeArc = 0; // 1 or 0
      let sweep = 1; // 1 or 0

      // Self edge.
      if (x1 === x2 && y1 === y2) {

        // Fiddle with this angle to get loop oriented.
        xRotation = -45;

        // Needs to be 1.
        largeArc = 1;

        // Change sweep to change orientation of loop.
        //sweep = 0;

        // Make drx and dry different to get an ellipse
        // instead of a circle.
        drx = 20;
        dry = 30;

        // For whatever reason the arc collapses to a point if the beginning
        // and ending points of the arc are the same, so kludge it.
        x2 = x2 + 1;
        y2 = y2 + 1;
      }
      return "M " + x1 + "," + y1 + " A " + drx + " " + dry + " " + xRotation + " " + largeArc + " " + sweep + " " + x2 + "," + y2;
    });

    gNodeEnter
      .attr("transform", function(d) {
        return "translate(" + d.x + "," + d.y + ")";
      });
  }
};

And the Corresponding csv file

TaskID,TaskType,Start,End,Caller,CallerType,Generation
construct,construct,0,6.5,Null,Null,0
start1,startsweep,0.4,0.7,construct,construct,1
start2,startsweep,0.8,4,construct,construct,1
start3,startsweep,1.5,3,construct,construct,1
start4,startsweep,2,3.3,construct,construct,1
start5,startsweep,2.8,4.9,construct,construct,1
start6,startsweep,3.4,4,construct,construct,1
start7,startsweep,4.1,5.6,construct,construct,1
start8,startsweep,5,6,construct,construct,1
start9,startsweep,5.1,5.7,construct,construct,1
start10,startsweep,6,6.3,construct,construct,1
start11,startsweep,1.2,1.7,start2,startsweep,2
start12,startsweep,1.7,2.9,start2,startsweep,2
start13,startsweep,2.2,3,start2,startsweep,2
start14,startsweep,3.1,3.9,start2,startsweep,2
start15,startsweep,3,4,start5,startsweep,2
start16,startsweep,5.1,5.4,start8,startsweep,2
start17,startsweep,1.3,1.5,start11,startsweep,3
start18,startsweep,1.9,2.5,start12,startsweep,3
start19,startsweep,3.1,3.8,start15,startsweep,3
start20,startsweep,5.2,5.3,start16,startsweep,3
start21,startsweep,1.35,1.4,start17,startsweep,4
start22,startsweep,3.15,3.75,start19,startsweep,4
start23,startsweep,5.2,5.25,start20,startsweep,4
start24,startsweep,3.15,3.25,start22,startsweep,5
start25,startsweep,3.2,3.3,start22,startsweep,5
start26,startsweep,3.25,3.7,start22,startsweep,5
start27,startsweep,3.6,3.7,start22,startsweep,5
start28,startsweep,3.3,3.5,start26,startsweep,6
start29,startsweep,3.4,3.45,start28,startsweep,7

Upvotes: 1

Views: 170

Answers (1)

Michael Rovinsky
Michael Rovinsky

Reputation: 7210

Seems to be that you run enter/exit on the same selection (nodesG). The correct code should be:

render() {
  ...
  const nodesG = svg.selectAll('g.node').data(nodesData);
  const newNodes = nodesG.enter().append('g').classed('node', true);
  newNodes.append('circle')...
  nodesG.exit().remove();
  ...
}

Upvotes: 1

Related Questions