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

时间:2015-10-07 09:13:35

标签: javascript jquery d3.js

我有一个粘力布局:http://jsfiddle.net/smqsusdw/

我有这个函数将一个节点拖动到一个位置:

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;  });

}

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

例如,我按下按钮,只有一个节点移动而所有其他节点都保持不变。

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

2 个答案:

答案 0 :(得分:5)

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

考虑到这一点,很明显,您的解决方案将无法按预期工作。使用图形元素上的过渡只会移动节点而不更新数据,也不需要任何力布局的参与。要获得所需的行为,您需要坚持计算和渲染的分离。这将使您免于实现鼠标事件模拟的需要。

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

}

请查看以下代码段或更新的More information in doc.,了解您的代码是否适用。



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>
&#13;
&#13;
&#13;

答案 1 :(得分:3)

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

这是一种做类似事情的方法,但使用过渡。这使您可以免费访问缓动,延迟和链接,因此很容易将其推广到更复杂的运动集。

在虚拟节点上使用转换作为计时器

  1. 使用独占命名空间创建一个虚拟节点(因此无法渲染)并对其进行过渡。
  2. 在所选数据元素上定义pxpy的getter,通过返回虚拟的假cxcy属性,透明地与转换挂钩节点正在转换。
  3. 在所选节点上调用dragstart。
  4. 在转换的end事件中,通过使用虚拟节点属性的当前值替换getter来进行清理。
  5. 将此结构包装在d3选择中,以便可以将其推广到节点的任意子集。
  6. 使用javascript Array.prototype.reduce方法链接任意数量的转换。
  7. 您可以继续点击按钮,然后将节点发送到随机位置。

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

    &#13;
    &#13;
    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}
                    });
                });
        }
    &#13;
    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;
            }
    &#13;
    <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>
    &#13;
    &#13;
    &#13;

    修改

    以下是一些可能性,这都归功于d3过渡的力量......

    &#13;
    &#13;
        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()
    &#13;
    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;
            }
    &#13;
    <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>
    &#13;
    &#13;
    &#13;