【问题标题】:How to simulate mouse move in D3 so when you drag nodes, other nodes move automatically?如何在 D3 中模拟鼠标移动,以便在拖动节点时,其他节点会自动移动?
【发布时间】:2016-01-04 10:52:48
【问题描述】:

我有一个粘性布局: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;  });

}

现在,当它执行此操作时,我希望它看起来像是在用鼠标拖动它。但是当我按下按钮时,只有选定的节点移动。无论如何要在节点上模拟鼠标拖动,以便其他相关节点似乎随之移动?

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

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

【问题讨论】:

  • 另外,当我在移动节点后拖动节点时,节点似乎又回到了之前的位置。我认为这是因为当您调用 force.drag 时,它会再次启动力物理。我希望能够在不使用鼠标的情况下模拟鼠标移动
  • mousemove 事件的任何属性都是“只读”。但是,如果您首先移动所选节点然后在其上触发 mousemove 事件(该节点应该是目标),它可能对您有用。只是一个想法。

标签: javascript jquery d3.js


【解决方案1】:

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

考虑到这一点,很明显,您的解决方案将无法按预期工作。在图形元素上使用转换只会移动节点而不更新数据并且不涉及力布局。要获得所需的行为,您需要坚持计算和渲染的解耦。这将使您无需实现鼠标事件的模拟。

这可以通过使用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;
    });

}

查看以下 sn-p 或更新的 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】:

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

这是一种非常相似的方法,但使用过渡。这使您也可以免费访问缓动、延迟和链接,因此很容易推广到更复杂的一组动作。

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

  1. 创建一个具有独占命名空间的虚拟节点(因此它不会被渲染)并在其上放置一个过渡。
  2. 为所选数据元素上的pxpy 定义getter,以透明地连接到转换,方法是在转换时返回虚拟节点的假cxcy 属性。李>
  3. 在选定节点上调用 dragstart。
  4. 在转换的 end 事件中,通过将 getter 替换为虚拟节点属性的当前值来进行清理。
  5. 将此结构包装在 d3 选择中,以便可以将其推广到任意节点子集。
  6. 使用 javascript Array.prototype.reduce 方法链接任意数量的转换。

您可以继续单击该按钮,它会将节点发送到随机位置。

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

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>

编辑

这里有更多的可能性,都是由于 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>

【讨论】:

  • 这真的很酷:)) 我喜欢你使用随机航点的方式以及一次移动多个节点的能力:)) 非常好的例子。正如您所说,“高积云”太快了,但感谢您的意见。很高兴看到不同角度的解决方案:))
  • @thisOneGuy 关键是虚拟节点上的转换以及数据元素上的吸气剂。它释放了 d3 数据绑定和转换的所有功能。如果你玩弄不同的缓动函数和不同的 x 和 y 动作时间,你可以得到一种功夫动作:P
  • 哇,另一个更新。我喜欢你的得意忘形,我完全一样:L
猜你喜欢
  • 2021-02-13
  • 2022-01-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多