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

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

本文关键字:节点 移动 拖动 其他 当你 D3 模拟 鼠标      更新时间:2023-09-26

我有一个粘性力布局: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力物理,相关节点也会随之移动。有没有办法模拟这个移动

要选择正确的方法,重要的是要知道,在D3的力布局的计算是从任何元素的实际渲染解耦。d3.layout.force()将根据指定的参数负责计算动作和位置。呈现将由注册在.force("tick", renderingHandler)的处理程序完成。这个函数将被force布局调用,并根据计算出的位置渲染元素。

考虑到这一点,很明显,您的解决方案不会像预期的那样工作。在图形元素上使用过渡只会移动节点,而不会更新数据,也不会涉及到力布局。为了获得期望的行为,您需要坚持计算和呈现的解耦。这将使您不必实现模拟鼠标事件。

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

查看下面的代码片段或更新后的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>

我一直在玩这个,所以我想我也可以把它贴出来。
@altocumulus对我来说太快了!

这里有一种方法可以做非常类似的事情,但是使用过渡。这让你可以免费使用放松,延迟和连锁,所以很容易推广到更复杂的动作集。

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

  1. 创建一个具有独占命名空间的虚拟节点(因此它不会被渲染)并在其上放置一个过渡。
  2. 在选定的数据元素上定义pxpy的getter,通过在转换时返回假的cxcy属性,透明地与转换挂钩。
  3. 在选中的节点上调用拖拽启动。
  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>