如何在D3中模拟鼠标移动,这样当你拖动节点时,其他节点会自动移动?

时间:2022-11-03 19:58:23

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

我有一个粘力布局: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 ?

现在,当它这样做时,我希望它看起来像我用鼠标拖动它。但是当我按下按钮时,只有所选节点移动。反正是否在节点上模拟mousedrag以便其他相关节点似乎随之移动?

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

但是当我将其中一个节点拖到一个位置时,由于D3力物理,相关节点会随之移动。有没有办法模拟这种运动

2 个解决方案

#1


5  

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.

要选择正确的方法,重要的是要知道在D3的力布局中,计算与任何元素的实际渲染分离。 d3.layout.force()将根据指定的参数计算运动和位置。渲染将由.force(“tick”,renderingHandler)注册的处理程序完成。此功能将在每个刻度线上由力布局调用,并根据计算的位置渲染元素。

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.

这可以通过使用d3.timer()来完成,它将重复调用一个函数,将移动节点的位置设置为其开始值和结束值之间的插值。在设置了这些值之后,该函数将激活强制布局以对其余节点执行其工作并调用渲染处理程序.tick(),这将更新整个布局。

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.

请查看以下代码段或更新的JSFiddle,以便对代码进行适当的调整。

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>

#2


3  

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

我正在玩这个,所以我想我也可以发布它。 @altocumulus对我来说太快了!

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. 创建一个具有独占命名空间的虚拟节点(因此它不会被渲染)并在其上进行转换。
  3. 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.
  4. 在所选数据元素上定义px和py的getter,通过在转换时返回虚拟节点的伪cx和cy属性,透明地与转换挂钩。
  5. Call dragstart on the selected node.
  6. 在所选节点上调用dragstart。
  7. On the end event of the transition, clean up by replacing the getters with the current value of the dummy node attributes.
  8. 在转换的结束事件中,通过使用虚拟节点属性的当前值替换getter来进行清理。
  9. Wrap this structure in a d3 selection so that it can be generalised to an arbitrary subset of the nodes.
  10. 将此结构包装在d3选择中,以便可以将其推广到节点的任意子集。
  11. Use the javascript Array.prototype.reduce method to chain an arbitrary number of transitions.
  12. 使用javascript Array.prototype.reduce方法链接任意数量的转换。

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.

如果使用d3样式数据绑定生成虚拟节点,则可以轻松地将其概括为一致地移动任意数量的节点。在以下示例中,将对固定属性进行过滤。

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

            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...

这里有一些可能性,都归功于d3过渡的力量......

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

            Object.defineProperties(d, {
                px: {
                    get: function() {return d.x = +n.attr("cx")},
                    configurable: true
                },
                py: {
                    get: function() {return d.y = +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 d.x + (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 d.y + (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>

#1


5  

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.

要选择正确的方法,重要的是要知道在D3的力布局中,计算与任何元素的实际渲染分离。 d3.layout.force()将根据指定的参数计算运动和位置。渲染将由.force(“tick”,renderingHandler)注册的处理程序完成。此功能将在每个刻度线上由力布局调用,并根据计算的位置渲染元素。

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.

这可以通过使用d3.timer()来完成,它将重复调用一个函数,将移动节点的位置设置为其开始值和结束值之间的插值。在设置了这些值之后,该函数将激活强制布局以对其余节点执行其工作并调用渲染处理程序.tick(),这将更新整个布局。

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.

请查看以下代码段或更新的JSFiddle,以便对代码进行适当的调整。

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>

#2


3  

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

我正在玩这个,所以我想我也可以发布它。 @altocumulus对我来说太快了!

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. 创建一个具有独占命名空间的虚拟节点(因此它不会被渲染)并在其上进行转换。
  3. 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.
  4. 在所选数据元素上定义px和py的getter,通过在转换时返回虚拟节点的伪cx和cy属性,透明地与转换挂钩。
  5. Call dragstart on the selected node.
  6. 在所选节点上调用dragstart。
  7. On the end event of the transition, clean up by replacing the getters with the current value of the dummy node attributes.
  8. 在转换的结束事件中,通过使用虚拟节点属性的当前值替换getter来进行清理。
  9. Wrap this structure in a d3 selection so that it can be generalised to an arbitrary subset of the nodes.
  10. 将此结构包装在d3选择中,以便可以将其推广到节点的任意子集。
  11. Use the javascript Array.prototype.reduce method to chain an arbitrary number of transitions.
  12. 使用javascript Array.prototype.reduce方法链接任意数量的转换。

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.

如果使用d3样式数据绑定生成虚拟节点,则可以轻松地将其概括为一致地移动任意数量的节点。在以下示例中,将对固定属性进行过滤。

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

            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...

这里有一些可能性,都归功于d3过渡的力量......

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

            Object.defineProperties(d, {
                px: {
                    get: function() {return d.x = +n.attr("cx")},
                    configurable: true
                },
                py: {
                    get: function() {return d.y = +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 d.x + (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 d.y + (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>