Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

d3 / svg layering issue when adding new nodes after a time

I am having an annoying issue with my d3 force directed map I'm building where I initially render the page with the nodes and links I need, then periodically check for new information via ajax. When I need a new node and link I draw them, which is fine

However because of the way SVG layers elements, new links draw over older nodes, so where I have nodes as circles and draw lines between them, any new nodes added draw over the top of the circles on older nodes. See image below:

layering issue

(http://i40.tinypic.com/4fx25j.gif)

I know it is not technically a d3 issue but there must be a way of fixing this. I did try deleting all the circles and redrawing them, but the issue is that the svg:g node it is attached to is too low in the layers so it is still drawn over.

Demo at jsfiddle - look at the following section

draw() {
   ...
}

as that is where the magic happens. http://jsfiddle.net/zuzzy/uwAhy/

I have simulated the ajax using a 5 second timer, it was easier for the demo.

Any ideas?

like image 342
zuzzy Avatar asked May 29 '13 13:05

zuzzy


3 Answers

As far as I am aware, you can only control the depth of an SVG element by it's position in the DOM.

So what might work for you is to create two groups <g id='lines'> and <g id='circles'>.

When you append your elements, all of the lines should be added to the first group, and all of the circles to the second.

You might have to alter the way that you add the elements, but so long as you make sure that the lines group appears before the circles group then you should be golden.

I apologise if this totally does not fit your implementation. I ran into a very similar problem and found the only resolution for me was to draw the 'lower' elements first.

like image 73
paul Avatar answered Sep 19 '22 21:09

paul


Worked first time! I had already grouped all my elements under one so I just replaced:

var vis = d3.select("body")    
.append("svg:svg")  
.attr("pointer-events", "all");  
.append('svg:g')

where i used vis.xxxx to render both links and circles, with

var vis = d3.select("body") 
.append("svg:svg")   
.attr("pointer-events", "all");  

var linkvis = vis.append('svg:g')  
.attr("id","link_elements");   

vis = vis.append('svg:g')   
.attr("id","node_elements");   

and referred to linkvis when drawing the links and vis drawing the circles.

(NB I know this should be a comment but I couldn't fit it in and I thought it might be helpful for someone. @Paul's answer has been marked as the answer)

like image 30
zuzzy Avatar answered Sep 20 '22 21:09

zuzzy


Another way of resolving this issue would be to use insert method as shown in following code.

 link.enter().insert("line",".node"); //Inserts link element before the first DOM element with class node.

Posting just because this may be helpful for other users who search solution for this question.

//Settings:
//width, height and the default radius of the circles
var w = 1024,
  h = 768,
  r = 10;




//test data - usually this is recieved via ajax
//Initial Data:
var hosts = eval({
  "ITEM003": {
    "name": "ITEM003",
    "parents": [],
    "status": 0,
    "hostgroup": "Secure"
  },
  "ITEM004": {
    "name": "ITEM004",
    "parents": [],
    "status": 0,
    "hostgroup": "Secure"
  },
  "CORE": {
    "name": "CORE",
    "parents": ["ITEM004", "ITEM003"],
    "status": 0,
    "hostgroup": "DMZ"
  }
});

var mylinks = eval({
  "0": ["CORE", "ITEM004"],
  "1": ["CORE", "ITEM003"]
});

//Data after update
var updated_hosts = eval({
  "ITEM003": {
    "name": "ITEM003",
    "parents": [],
    "status": 0,
    "hostgroup": "Secure"
  },
  "ITEM004": {
    "name": "ITEM004",
    "parents": [],
    "status": 0,
    "hostgroup": "Secure"
  },
  "CORE": {
    "name": "CORE",
    "parents": ["ITEM004", "ITEM003"],
    "status": 0,
    "hostgroup": "DMZ"
  },
  "REMOTE": {
    "name": "REMOTE",
    "parents": [],
    "status": 0,
    "hostgroup": ""
  }
});

var updated_mylinks = eval({
  "0": ["CORE", "ITEM004"],
  "1": ["CORE", "ITEM003"],
  "2": ["CORE", "REMOTE"]
});



//I define these here so they carry between functions - not really necessary in this jsfiddle probably
window.link = undefined;
window.node = undefined;




//make up my node object
window.nodeArray = [];
window.node_hash = [];

for (var key in hosts) {
  var a = {
    id: "node_" + hosts[key].name,
    labelText: hosts[key].name,
    status: hosts[key].status,
    hostgroup: hosts[key].hostgroup,
    class: "node realnode",
    iconimage: hosts[key].iconimage,
    added: true
  };
  nodeArray.push(a);
  node_hash[key] = a;
}

//make up my link object
window.linkArray = [];

for (var key in mylinks) {
  var linkcolor = "#47CC60";

  var a = {
    source: node_hash[mylinks[key][0]],
    target: node_hash[mylinks[key][1]],
    color: linkcolor,
    class: "link reallink"
  };
  linkArray.push(a);
}


//make up my node text objects
//these are just more nodes with a different class
//we will append text to them later
//we also add the links to the linkArray now to bind them to their real nodes
window.text_hash = [];

for (var key in hosts) {
  var a = {
    id: "label_" + hosts[key].name,
    text: hosts[key].name,
    color: "#ffffff",
    size: "6",
    class: "node label",
    added: true
  };
  nodeArray.push(a);
  text_hash[key] = a;
}

//because the text labels are in the same order as the
//original nodes we know that node_hash[0] has label text_hash[0]
//it doesn't matter which we iterate through here

for (var key in text_hash) {
  var a = {
    source: node_hash[key],
    target: text_hash[key],
    class: "link label"
  };
  linkArray.push(a);
}



//set up the environment in a div called graph using the settings baove 
window.vis = d3.select("body")
  .append("svg:svg")
  .attr("height", 500)
  .attr("width", 500)
  .attr("pointer-events", "all")
  .append('svg:g')




//object to interact with the force libraries in d3
//the settings here set how the nodes interact
//seems a bit overcomplicated but it stops the diagram going nuts!
window.force = d3.layout.force()
  .friction("0.7")
  .gravity(function(d, i) {
    if (d.class == "link reallink") {
      return "0.95";
    } else {
      return "0.1";
    }

  })
  .charge(function(d, i) {
    if (d.class == "link reallink") {
      return "-1500";
    } else {
      return "-300";
    }

  })
  .linkDistance(function(d) {
    if (d.class == "link reallink") {
      return "120";
    } else {
      return "35";
    }

  })
  .linkStrength(function(d) {
    if (d.class == "link reallink") {
      return "8";
    } else {
      return "6";
    }
  })
  .nodes(nodeArray)
  .links(linkArray)
  .on("tick", tick)

node = vis.selectAll(".node");
link = vis.selectAll(".link");

//create the objects and run it
draw();

for (key in nodeArray) {
  nodeArray[key].added = false;
}

//wait 5 seconds then update the diagram TO ADD A NODE
setTimeout(function() {
  //update the objects
  //vis.selectAll("g.node").data(nodeArray).exit().transition().ease("elastic").remove();
  //vis.selectAll("line").data(linkArray).exit().transition().ease("elastic").remove();


  var a = {
    id: "node_REMOTE",
    labelText: "REMOTE",
    status: "0",
    hostgroup: "",
    class: "node realnode",
    iconimage: "",
    added: true
  };
  nodeArray.push(a);
  node_hash["REMOTE"] = a;



  var linkcolor = "#47CC60";
  var a = {
    source: node_hash["CORE"],
    target: node_hash["REMOTE"],
    color: linkcolor,
    class: "link reallink"
  };
  linkArray.push(a);



  //make up my node text objects


  var a = {
    id: "label_REMOTE",
    text: "REMOTE",
    color: "#000000",
    size: "6",
    class: "node label",
    added: true
  };
  nodeArray.push(a);
  text_hash["REMOTE"] = a;


  var a = {
    source: node_hash["REMOTE"],
    target: text_hash["REMOTE"],
    class: "link label"
  };
  linkArray.push(a);


  //redraw it
  draw();

}, 5000);




//----- functions for drawing and tick below


function draw() {

  link = link.data(force.links(), function(d) {
    return d.source.id + "-" + d.target.id;
  });
  node = node.data(force.nodes(), function(d) {
    return d.id;
  });


  //create the link object using the links object in the json
  //link = vis.selectAll("line").data(linkArray);
  link.enter().insert("line", ".node")
    .attr("stroke-width", '0')
    .transition()
    .duration(1000)
    .ease("bounce")
    .attr("stroke-width", function(d, i) {
      if (d.class == 'link reallink') {
        return '3';
      } else {
        return '0';
      };
    })
    .style("stroke", function(d, i) {
      return d.color;
    })
    .attr("class", function(d, i) {
      return d.class;
    });


  //node = vis.selectAll("g").data(nodeArray);
  node.enter().append("svg:g")
    .attr("class", function(d) {
      return d.class
    })
    .attr("id", function(d) {
      return d.id
    })
    .call(force.drag);


  //append to each node an svg circle element
  vis.selectAll(".realnode").filter(function(d) {
      return d.added;
    })
    .append("svg:circle")
    .attr("r", "0")
    .transition()
    .duration(1000)
    .ease("bounce")
    .attr("r", "6")
    .style("fill", "#000000")
    .style("stroke", function(d) {
      return d.color;
    })
    .style("stroke-width", "4");



  //append to each node the attached text desc
  vis.selectAll(".label").filter(function(d) {
      return d.added;
    })
    .append("svg:text")
    .attr("text-anchor", "middle")
    .attr("fill", "black")
    .style("pointer-events", "none")
    .attr("font-size", "9px")
    .attr("font-weight", "100")
    .text(function(d) {
      return d.text;
    })
    .attr("transform", "rotate(180)")
    .transition()
    .duration(1000)
    .ease("bounce")
    .attr("transform", "rotate(0)");


  node.exit().transition().ease("elastic").remove();
  link.exit().transition().ease("elastic").remove();

  //activate it all - initiate the nodes and links
  force.start();

}





function tick() {
  node.attr("cx", function(d) {
      return d.x = Math.max(r + 15, Math.min(w - r - 15, d.x));
    })
    .attr("cy", function(d) {
      return d.y = Math.max(r + 15, Math.min(h - r - 15, d.y));
    })
    .attr("transform", function(d) {
      return "translate(" + d.x + "," + d.y + ")";
    });


  link.data(linkArray).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;
    });
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
like image 27
Gilsha Avatar answered Sep 20 '22 21:09

Gilsha