简体   繁体   中英

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

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 dx; }) .attr("cy", function(d) { return dy; }); } 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> 

I was playing around with this so I thought I may as well post it as well.
@altocumulus was too fast for me!

Here is a way to do a very similar thing but using a transition . This allows you to access easing, delays and chaining for free as well, so it's easy to generalise to a more complex set of movements.

Using a transition on dummy node as a timer

  1. Create a dummy node with an exclusive namespace (so it won't be rendered) and put a transition on it.
  2. Define getters for px and py on the chosen data element, to transparently hook up with the transition, by returning the fake cx and cy attributes of the dummy node while they are transitioning.
  3. Call dragstart on the selected node.
  4. On the end event of the transition, clean up by replacing the getters with the current value of the dummy node attributes.
  5. Wrap this structure in a d3 selection so that it can be generalised to an arbitrary subset of the nodes.
  6. Use the javascript Array.prototype.reduce method to chain an arbitrary number of transitions.

You can keep clicking the button and it sends the node to random locations.

If you generate the dummy nodes using d3 style data binding then you can easily generalise it to move any number of nodes in unison. In the following example they are filtered on the fixed property.

 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 = 500, height = 190, steps = function(){return +d3.select("#steps-selector").property("value")}; var force = d3.layout.force() .size([width, height]) .charge(-100) .linkDistance(6) .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", 6) .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 dx; }) .attr("cy", function(d) { return dy; }); force.alpha(0.1) } 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 ns = "CB:emit/drag/transition/or-whatever-you-feel-like", shadowNodes = d3.select("body").selectAll("emitDrag") .data(graph.nodes.filter(function(d){return d.fixed})), shadowedData = []; shadowNodes.enter().append(function(){return document.createElementNS(ns, "emitDrag")}); shadowNodes.each(function(d, i){ var n = d3.select(this); shadowedData[i] = d; dragstart.call(node.filter(function(s){return s === d;}).node(), d); d.fixed = true; n.attr({cx: dx, cy: dy}); Object.defineProperties(d, { px: { get: function() {return +n.attr("cx")}, configurable: true }, py: { get: function() {return +n.attr("cy")}, configurable: true } }); }); force.start(); d3.range(steps()).reduce(function(o, s){ return o.transition().duration(750).ease("cubic") .attr({ cx: function(){return (1+3*Math.random())*width*0.2}, cy: function(){return (1+3*Math.random())*height*0.2} }) },shadowNodes) .each("end", function(d, i){ var n = d3.select(this); Object.defineProperties(shadowedData[i], { px: {value: +n.attr("cx"), writable: true}, py: {value: +n.attr("cy"), writable: true} }); }); } 
 body { margin: 0; } .link { stroke: #000; stroke-width: 1.5px; } .node { cursor: move; fill: #ccc; stroke: #000; stroke-width: 1.5px; } .node.fixed { fill: #f00; } button, input {display: inline-block} .input { position: absolute; top: 0; left: 0; /*white-space: pre;*/ margin: 0; } 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> <div class="input"> <button onclick = 'positionnodes()'> select the nodes to include then click me</button> steps <input id="steps-selector" onchange = 'positionnodes()' type="number" name="steps" value = 3 min="1" max="100"/> </div> 


EDIT

Here are a few more possibilities, all due to the power of d3 transitions...

  var graph ={ "nodes": [ {"x": 469, "y": 410, move: true}, {"x": 493, "y": 364}, {"x": 442, "y": 365}, {"x": 467, "y": 314}, {"x": 477, "y": 248, move: true}, {"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, move: true} ], "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 = 500, height = 190, steps = function(){return +d3.select("#steps-selector").property("value")}; var inputDiv = d3.select("#input-div"), tooltip = (function tooTip() { var tt = d3.select("body").append("div") .attr("id", "tool-tip") .style({ position: "absolute", color: "black", background: "rgba(0,0,0,0)", display: "none" }); return function(message) { return message ? function() { var rect = this.getBoundingClientRect(); tt .style({ top: (rect.bottom + 6) + "px", left: (rect.right + rect.left) / 2 + "px", width: "10px", padding: "0 1em 0 1em", background: "#ccc", 'border-radius': "2px", display: "inline-block" }) .text(message) }: function() { tt .style({ display: "none" }) } } })(), easeings = ["linear", "quad", "cubic", "sin", "exp", "circle", "elastic", "back", "bounce"], xEase = d3.ui.select({ base: d3.select("#input-div"), oninput: positionnodes, data: easeings, initial: "bounce", onmouseover: tooltip("x"), onmouseout: tooltip() }), yEase = d3.ui.select({ base: d3.select("#input-div"), oninput: positionnodes, data: easeings, initial: "circle", onmouseover: tooltip("y"), onmouseout: tooltip() }), t = (function(){ var s = d3.select("#input-div").selectAll(".time") .data([{name: "tx", value: 0.75}, {name: "ty", value: 1.6}]) .enter().append("input") .attr({ id: function(d){return d.name + "-selector"}, type: "number", name: function(d){return d.name}, value: function(d){return d.value}, min: "0.1", max: "5", step: 0.5 }) .on("change", positionnodes) .each(function(d){ d3.select(this).on("mouseover", tooltip(d.name)) }) .on("mouseout", tooltip()); return function(){ var values = []; s.each(function(){ values.push(d3.select(this).property("value") * 1000); }); return values; } })(); var force = d3.layout.force() .size([width, height]) .charge(-100) .linkDistance(6) .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", 6) .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 dx; }) .attr("cy", function(d) { return dy; }); force.alpha(0.1) } function dblclick(d) { d3.select(this).classed("fixed", d.move = false); } function dragstart(d) { d3.select(this).classed("fixed", d.move = true); } function positionnodes(){ var ns = "CB:emit/drag/transition/or-whatever-you-feel-like", transitions = d3.select("body").selectAll("transitions") .data([graph.nodes.filter(function(d){return d.move})]), transitionsEnter = transitions.enter().append(function(){ return document.createElementNS(ns, "transitions") }), shadowNodes = transitions.selectAll("emitDrag") .data(function(d){return d}), shadowedData = []; shadowNodes.enter().append(function(){ return document.createElementNS(ns, "emitDrag") }); shadowNodes.each(function(d, i){ var n = d3.select(this); shadowedData[i] = d; dragstart.call(node.filter(function(s){return s === d;}).node(), d), endAll = d3.cbTransition.endAll(); n.attr({cx: dx, cy: dy}); Object.defineProperties(d, { px: { get: function() {return dx = +n.attr("cx")}, configurable: true }, py: { get: function() {return dy = +n.attr("cy")}, configurable: true } }); }); force.start(); d3.range(steps()).reduce(function(o){ return (o.transition("cx").duration(t()[0]).ease(xEase.value()) .attr({ cx: function(d){ // return dx + (Math.random() - 0.5) * width/5 return (1+3*Math.random())*width*0.2 } })) },shadowNodes) .call(cleanUp, "px", "cx"); d3.range(steps()).reduce(function(o){ return (o.transition("cy").duration(t()[1]).ease(yEase.value()) .attr({ cy: function(d){ // return dy + (Math.random() - 0.5) * height/5 return (1+3*Math.random())*height*0.2 } })) },shadowNodes) .call(cleanUp, "py", "cy"); function cleanUp(selection, getter, attribute){ selection.each("end.each", function(d, i){ var n = d3.select(this); Object.defineProperty(shadowedData[i], getter, { value: +n.attr(attribute), writable: true }); }) .call(endAll, function(){ transitions.remove(); }, "move-node"); } } positionnodes() 
 body { margin: 0; position: relative; } .link { stroke: #000; stroke-width: 1.5px; } .node { cursor: move; fill: #ccc; stroke: #000; stroke-width: 1.5px; } .node.fixed { fill: #f00; } button, input {display: inline-block} .input-div { position: absolute; top: 0; left: 0; /*white-space: pre;*/ margin: 0; } 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> <script src="https://rawgit.com/cool-Blue/d3-lib/master/transitions/end-all/1.0.0/endAll.js" charset="UTF-8"></script> <script src="https://rawgit.com/cool-Blue/d3-lib/master/inputs/select/select.js" charset="UTF-8"></script> <div id="input-div"> <button onclick = 'positionnodes()'> select the nodes to include then click me</button> steps <input id="steps-selector" onchange = 'positionnodes()' type="number" name="steps" value = 10 min="1" max="100"/> </div> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM