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

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

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

function positionnodes(){ force.stop(); node.each(function(d, i){ if(i===1){ d.fixed = true; dx = 100; dy = 100; } }).transition().duration(1000).attr("cx", function(d){ return dx }).attr("cy", function(d){ return dy }); 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力物理,相关节点会随之移动。 有没有办法模拟这种运动

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

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

这可以通过使用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 dx; }) .attr("cy", function(d) { return dy; }); } function dblclick(d) { d3.select(this).classed("fixed", d.fixed = false); } function dragstart(d) { d3.select(this).classed("fixed", d.fixed = true); } function positionnodes(){ var move = graph.nodes[1], // the node to move around duration = 1000, // duration of the movement finalPos = { x: 100, y: 100 }, interpolateX = d3.interpolateNumber(move.x, finalPos.x), interpolateY = d3.interpolateNumber(move.y, finalPos.y); // We don't want the force layout to mess with our node. move.fixed = true; // Move the node by repeatedly determining its position. d3.timer(function(elapsed) { // Because the node should remain fixed, the previous position (.px, .py) // needs to be set to the same value as the new position (.x, .y). This way // the node will not have any inherent movement. move.x = move.px = interpolateX(elapsed / duration); move.y = move.py = interpolateY(elapsed / duration); // Re-calculate the force layout. This will also invoke tick() // which will take care of the rendering. force.start(); // Terminate the timer when the desired duration has elapsed. return elapsed >= duration; }); } 
 .link { stroke: #000; stroke-width: 1.5px; } .node { cursor: move; fill: #ccc; stroke: #000; stroke-width: 1.5px; } .node.fixed { fill: #f00; } 
   

我正在玩这个,所以我想我也可以发布它。
@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 dx; }) .attr("cy", function(d) { return dy; }); force.alpha(0.1) } function dblclick(d) { d3.select(this).classed("fixed", d.fixed = false); } function dragstart(d) { d3.select(this).classed("fixed", d.fixed = true); } function positionnodes(){ var ns = "CB:emit/drag/transition/or-whatever-you-feel-like", shadowNodes = d3.select("body").selectAll("emitDrag") .data(graph.nodes.filter(function(d){return d.fixed})), shadowedData = []; shadowNodes.enter().append(function(){return document.createElementNS(ns, "emitDrag")}); shadowNodes.each(function(d, i){ var n = d3.select(this); shadowedData[i] = d; dragstart.call(node.filter(function(s){return s === d;}).node(), d); d.fixed = true; n.attr({cx: dx, cy: dy}); Object.defineProperties(d, { px: { get: function() {return +n.attr("cx")}, configurable: true }, py: { get: function() {return +n.attr("cy")}, configurable: true } }); }); force.start(); d3.range(steps()).reduce(function(o, s){ return o.transition().duration(750).ease("cubic") .attr({ cx: function(){return (1+3*Math.random())*width*0.2}, cy: function(){return (1+3*Math.random())*height*0.2} }) },shadowNodes) .each("end", function(d, i){ var n = d3.select(this); Object.defineProperties(shadowedData[i], { px: {value: +n.attr("cx"), writable: true}, py: {value: +n.attr("cy"), writable: true} }); }); } 
 body { margin: 0; } .link { stroke: #000; stroke-width: 1.5px; } .node { cursor: move; fill: #ccc; stroke: #000; stroke-width: 1.5px; } .node.fixed { fill: #f00; } button, input {display: inline-block} .input { position: absolute; top: 0; left: 0; /*white-space: pre;*/ margin: 0; } 
  
steps