user2832099
user2832099

Reputation: 13

D3Js updating wrong elements using call

I am new to D3JS and you can find the code at http://jsfiddle.net/3n8wD/

I am a facing an issue and any pointers would help

When I move the line it separates as expected, but as soon as I try to move the circles away it jumps back to the line.

On inspecting the Array, looks like the circle array is updating as I am moving the link, not sure what is causing that.

Any help on this would be highly appreciated. below is the code that i have on jsfiddle

var width = 960,
    height = 500;
graph1 = {"nodes":[{"x":444,"y":275},{"x":378,"y":324}],"links":[{"source":1,"target":0}]}


var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);
var onDraggable = function(d1) {
          console.log(d1);

          d1.x = d3.event.x, d1.y = d3.event.y;
          d3.select(this).attr("cx", d1.x).attr("cy", d1.y);
          //console.log(d1);
          link.filter(function(l) { return l.source === d1; }).attr("x1", d3.event.x).attr("y1", d3.event.y);
          link.filter(function(l) { return l.target === d1; }).attr("x2", d3.event.x).attr("y2", d3.event.y);


        }
var drag = d3.behavior.drag()
      .origin(function(d) { return d; })
      .on("drag", onDraggable)




  var onDraggable1 = function(d) {
          //d.x1 = d3.event.x1, d.y1 = d3.event.y1, d.x2=d3.event.x2, y2=d3.event.y2;
          var mouseX = d3.mouse(this)[0];
          var mouseY = d3.mouse(this)[1];
          var relativeX = mouseX-d.source.x;
          var relativeY = mouseY-d.source.y;
          console.log(d);

          //console.log(d);
         // d3.select(this).attr("x1", d3.event.x).attr("y1", d3.event.y).attr("x2", d3.event.x).attr("y2", d3.event.y);
          d.source.x= d.source.x+relativeX;
          d.source.y = d.source.y+relativeY;
          d.target.x= d.target.x+relativeX;
          d.target.y = d.target.y+relativeY;

          d3.select(this).attr("x1", d.source.x).attr("y1", d.source.y);
          d3.select(this).attr("x2", d.target.x).attr("y2", d.target.y);

        }
var drag1 = d3.behavior.drag()
      .origin(function(d) { return d; })
      .on("drag", onDraggable1);

  graph1.links.forEach(function(d) {
    d.source = graph1.nodes[d.source];
    d.target = graph1.nodes[d.target];
  });




var node = svg.append("g").attr("class","node")
    .selectAll("circle")
      .data(graph1.nodes)
    .enter().append("circle")
      .attr("r", 4)
      .attr("cx", function(d) { return d.x; })
      .attr("cy", function(d) { return d.y; })
      .call(drag);

var link = svg.append("g").attr("class","link")
    .selectAll("line")
      .data(graph1.links)
    .enter().append("line")
      .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; })
      .attr("stroke-width", 2)
      .attr("stroke", "black")
      .call(drag1);

Upvotes: 1

Views: 501

Answers (1)

AmeliaBR
AmeliaBR

Reputation: 27544

The reason that your nodes are "jumping" to the end of the line, even after the line has been moved away, is because the data object for the node is the same object as the source/target data object for the line. The d value for the node and the d.source or d.target value for the link are just pointers (references) to the same object in the browser's memory. Everything you do to that data object is reflected in both the link and the node. That's what makes the node-drag function work: you change the data object directly, and then you update the position of the line and the circle to match their already-changed data value.

So, even though you don't update the circle's cx and cy position at the time you move the line, the statements d.source.x = etc. in the line drag method are setting the d.x and d.y values for the nodes. Then in the node drag method, when you access these values to determine the new position, the movement is determined relative to the end of the line, not to the position of the circle on screen.

So how do you get the behaviour you want, to separate the line from the node? You need to create a new data object for the line's source and target when you start to drag it, one that no longer references the nodes' data objects. You only need to do this once, at the start of the drag, so you use the dragstart event of the drag behaviour object:

var drag1 = d3.behavior.drag()
      .origin(function(d) { return d; })
      .on("dragstart", separateNodes);
      .on("drag", onDraggable1);

The separateNodes method is going to be quite short. All we have to do is, for both the target and the source of the link, create a new data object that is a copy of the existing target/source object (but which can be edited independently of the original object). Javascript doesn't have any default methods for copying an object (although various extensions do), but if the object in question just consists of x and y attributes, it is easy to do.

var separateNodes = function(d) {
    //create new data objects that are copies of d.source and d.target
    var newSource = {x:d.source.x, y:d.source.y};
    var newTarget = {x:d.target.x, y:d.target.y};

    //set the new objects as the target/source of this link
    d.source = newSource; 
    d.target = newTarget;
}

You could even skip the variable declarations and just merge it all into one line for source and one for target, but I find it easier to understand like this. I should mention, however, that this approach works because x and y are both just simple data types (numbers), not pointers to objects. So when it says x:d.source.x, you are actually getting a new copy of that number which you can change without having it affect the original object that contained that number.

If, however, your nodes and links also have a value property that is a complex data object, then you have to decide what you want to do when the line separates from the node: do you want to create a copy of the entire value data object, or maintain the link to the original object, or replace it with a null (empty/undefined) object? The first case is complicated, and depends on the data structure of the value object; for the second you don't need to do anything (except remember that you've got multiple pointers to the same object); and for the last case you just add a value:null, attribute to the newSource and newTarget object definitions in your separateNodes function.

Hope that all makes sense,
--ABR

Upvotes: 2

Related Questions