Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
345 views
in Technique[技术] by (71.8m points)

javascript - How to simulate mouse move in D3 so when you drag nodes, other nodes move automatically?

I have a sticky force layout : http://jsfiddle.net/smqsusdw/

I have this function that drags one node to a position :

function positionnodes(){

     force.stop();
     node.each(function(d, i){
         if(i===1){      

         d.fixed = true;
         d.x = 100;
         d.y = 100;
         }
     }).transition().duration(1000).attr("cx", function(d){ return d.x }).attr("cy", function(d){ return d.y });

    link.transition().duration(1000)
                      .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;  });

}

Now when it does this I want it to look like I am dragging it with my mouse. But when I press the button only the chosen node moves. Is there anyway to simulate a mousedrag on the node so that the other related nodes seem to move with it ?

For example, I press the button, only one node moves and all the others stay put.

But when I drag one of the nodes to a position the related nodes kind of move with it due to the D3 force physics. Is there a way to simulate this movement

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

To choose the right approach it is important to know that in D3's force layout the calculations are decoupled from the actual rendering of any elements. d3.layout.force() will take care of calculating movements and positions according to the specified parameters. The rendering will be done by the handler registered with .force("tick", renderingHandler). This function will get called by the force layout on every tick and render the elements based on the calculated positions.

With this in mind it becomes apparent, that your solution will not work as expected. Using transitions on the graphical elements will just move the nodes around without updating the data and without any involvement of the force layout. To get the desired behavior, you need to stick to the decoupling of calculations and rendering. This will free you from the need to implement a simulation of mouse events.

This could be done by using a d3.timer(), which will repeatedly invoke a function setting the moving node's position to the interpolated values between its start and end values. After having set these values, the function will activate the force layout to do its work for the rest of the nodes and invoke the rendering handler .tick(), which will update the entire layout.

function positionnodes(){

    var move = graph.nodes[1],  // the node to move around
        duration = 1000,        // duration of the movement
        finalPos = { x: 100, y: 100 },
        interpolateX = d3.interpolateNumber(move.x, finalPos.x),
        interpolateY = d3.interpolateNumber(move.y, finalPos.y);

    // We don't want the force layout to mess with our node.
    move.fixed = true;  

    // Move the node by repeatedly determining its position.
    d3.timer(function(elapsed) {

        // Because the node should remain fixed, the previous position (.px, .py)
        // needs to be set to the same value as the new position (.x, .y). This way
        // the node will not have any inherent movement.
        move.x = move.px = interpolateX(elapsed / duration); 
        move.y = move.py = interpolateY(elapsed / duration); 

        // Re-calculate the force layout. This will also invoke tick()
        // which will take care of the rendering.
        force.start();

        // Terminate the timer when the desired duration has elapsed.
        return elapsed >= duration;
    });

}

Have a look at the following snippet or the updated JSFiddle for a working adaption of your code.

var graph  ={
  "nodes": [
    {"x": 469, "y": 410},
    {"x": 493, "y": 364},
    {"x": 442, "y": 365},
    {"x": 467, "y": 314},
    {"x": 477, "y": 248},
    {"x": 425, "y": 207},
    {"x": 402, "y": 155},
    {"x": 369, "y": 196},
    {"x": 350, "y": 148},
    {"x": 539, "y": 222},
    {"x": 594, "y": 235},
    {"x": 582, "y": 185},
    {"x": 633, "y": 200}
  ],
  "links": [
    {"source":  0, "target":  1},
    {"source":  1, "target":  2},
    {"source":  2, "target":  0},
    {"source":  1, "target":  3},
    {"source":  3, "target":  2},
    {"source":  3, "target":  4},
    {"source":  4, "target":  5},
    {"source":  5, "target":  6},
    {"source":  5, "target":  7},
    {"source":  6, "target":  7},
    {"source":  6, "target":  8},
    {"source":  7, "target":  8},
    {"source":  9, "target":  4},
    {"source":  9, "target": 11},
    {"source":  9, "target": 10},
    {"source": 10, "target": 11},
    {"source": 11, "target": 12},
    {"source": 12, "target": 10}
  ]
}





var width = 960,
    height = 500;

var force = d3.layout.force()
    .size([width, height])
    .charge(-400)
    .linkDistance(40)
    .on("tick", tick);

var drag = force.drag()
    .on("dragstart", dragstart);

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

var link = svg.selectAll(".link"),
    node = svg.selectAll(".node");

//d3.json("graph.json", function(error, graph) {
 // if (error) throw error;

  force
      .nodes(graph.nodes)
      .links(graph.links)
      .start();

  link = link.data(graph.links)
    .enter().append("line")
      .attr("class", "link");

  node = node.data(graph.nodes)
    .enter().append("circle")
      .attr("class", "node")
      .attr("r", 12)
      .on("dblclick", dblclick)
      .call(drag);
//});

function tick() {
  link.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; });

  node.attr("cx", function(d) { return d.x; })
      .attr("cy", function(d) { return d.y; });
}

function dblclick(d) {
  d3.select(this).classed("fixed", d.fixed = false);
}

function dragstart(d) {
  d3.select(this).classed("fixed", d.fixed = true);
}


function positionnodes(){
    
    var move = graph.nodes[1],  // the node to move around
        duration = 1000,        // duration of the movement
        finalPos = { x: 100, y: 100 },
        interpolateX = d3.interpolateNumber(move.x, finalPos.x),
        interpolateY = d3.interpolateNumber(move.y, finalPos.y);
    
    // We don't want the force layout to mess with our node.
    move.fixed = true;  
    
    // Move the node by repeatedly determining its position.
    d3.timer(function(elapsed) {
        
        // Because the node should remain fixed, the previous position (.px, .py)
        // needs to be set to the same value as the new position (.x, .y). This way
        // the node will not have any inherent movement.
        move.x = move.px = interpolateX(elapsed / duration); 
        move.y = move.py = interpolateY(elapsed / duration); 
        
        // Re-calculate the force layout. This will also invoke tick()
        // which will take care of the rendering.
        force.start();
        
        // Terminate the timer when the desired duration has elapsed.
        return elapsed >= duration;
    });

}
.link {
  stroke: #000;
  stroke-width: 1.5px;
}

.node {
  cursor: move;
  fill: #ccc;
  stroke: #000;
  stroke-width: 1.5px;
}

.node.fixed {
   fill: #f00;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<button onclick = 'positionnodes()'> click me</button>

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

2.1m questions

2.1m answers

60 comments

56.8k users

...