强制布局拖动行为,缩放时有过渡但拖动时没有过渡

Force layout drag behaviour with transition on zoom but no transition on drag

我觉得这个问题很简单。我有这个站点供您演示我的意思:http://arda-maps.org/familytree/ 因此,如果您在屏幕上添加一些人,您可以 拖动 缩放 视图。

变焦完全没问题。变焦的持续时间很棒。但是我不喜欢拖动时的持续时间,并且想在那里禁用它。基本上这是代码:

g.transition().duration(450).attr("transform", "translate(" + zoombuttonTranslate + ")scale(" + zoombuttonScale + ")");

所以问题是如何在拖动事件中禁用 transition/duration?这甚至有可能吗?

第一阶段

我认为这很接近... 只需要验证它会很好地处理节点上的拖动行为。

策略

  • 使用d3.event.sourceEvent.type检查mousemove
  • 使用d3.transform
  • 增强当前变换状态
  • transition translate and scale 用于鼠标滚轮事件,无 transition 用于鼠标按钮事件

工作示例

var width = 600, height = 200-16,
    margin = {top: 25, right: 5, bottom: 5, left: 5},
    w = width - margin.left - margin.right,
    h = height - margin.top - margin.bottom,

    zoom = d3.behavior.zoom().scaleExtent([0.4, 4])
      .on("zoom", zoomed),
    svg = d3.select("#viz").attr({width: width, height: height})
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .call(zoom),
    transText = svg.append("text")
      .text("transform = translate ( margin.left , margin.top )")
      .style("fill", "#5c5c5c")
      .attr("dy", "-.35em")
    surface = svg.append("rect")
      .attr({width: w, height: h})
      .style({"pointer-events": "all", fill: "#ccc", "stroke-width": 3, "stroke": "#fff"}),
    surfaceText = svg.append("text")
      .text("pointer-events: all")
      .style("fill", "#5c5c5c")
      .attr({"dy": "1em", "dx": ".2em"})
    content = svg.append("g").attr("id", "content")
      .attr("transform", "translate(0,0)"),
    contentText = content.append("text")
    .text("transform = translate ( d3.event.translate ) scale ( d3.event.scale )")
    .style("fill", "#5c5c5c")
    .attr({"dy": 50, "dx": 20})
    content.selectAll("rect")
      .data([[20,60],[140,60]])
      .enter().append("rect")
      .attr({height: 50, width: 50})
      .style({"stroke-width": 3, "stroke": "#ccc"})
      .each(function(d){
        d3.select(this).attr({x: d[0], y: d[1]});
      });

  function zoomStart(){

  }
  function zoomed(){
    return d3.event.sourceEvent.buttons ? zoomDrag.call(this) : zoomScale.call(this)
  }
  function zoomDrag(){
  var t = d3.transform(content.attr("transform"));
    t.translate = d3.event.translate;
    content.attr("transform", t.toString());
  }
  function zoomScale(){
    var t = d3.transform(content.attr("transform"));
    t.translate = d3.event.translate; t.scale = d3.event.scale;
    content.transition().duration(450).attr("transform", t.toString());
  }
svg {
      outline: 1px solid #282f51;
      pointer-events: all;
    }
    g {
      outline: 1px solid red;
      shape-rednering: "geometricPrecision";
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg id="viz"></svg>


第二阶段

合并 FDG

由于 FDG 必须在 canvas 容器内,因此有必要阻止节点级事件传播到 canvas。这是在 OP 代码中通过使用自定义拖动行为完成的,在 dragstart 上停止传播并添加一些 force.drag 行为(加上设置 d.fixed = true ). This is great if you don't mind losing some of theforce.dragfeatures like sticking nodes on mouseover. This is nice for capturing small, energetic nodes though. So, in order to get the best of both worlds, you can hook theforce.drag` 行为。

策略

  • 应用与第一阶段相同的原则,但对鼠标滚轮事件进行跨浏览器测试。
  • 向节点添加标准force.drag
  • 挂钩 force.drag 以添加自定义行为
  • 仅修复 shift-drag (或 shift-dragend)[= 上的节点78=]
  • 对于触摸设备,如果在 dragstart
  • 时触摸 > 1,也会修复节点

最后两点允许在需要时轻松释放固定节点。

force.drag勾

        //hook force.drag behaviour
        var stdDragStart = force.drag().on("dragstart.force");
        force.drag()
            .on("dragstart", function(d){
                //prevent dragging on the nodes from dragging the canvas
                d3.event.sourceEvent.stopPropagation();
                stdDragStart.call(this, d);
            });

工作示例

//debug panel/////////////////////////////////////////////////////////////////////////////
var alpha = d3.select("#alpha").text("waiting..."),
  cog = d3.select("#wrapAlpha").insert("i", "#fdg").classed("fa fa-cog fa-spin", true).datum({instID: null}),
  fdgInst = d3.select("#fdg");
elapsedTime = ElapsedTime("#panel", {margin: 0, padding: 0})
 .message(function (id) {
  return 'fps : ' + d3.format(" >8.3f")(1/this.aveLap())
 });
elapsedTime.consoleOn = true;

alpha.log = function(e, instID) {
 elapsedTime.mark().timestamp();
 alpha.text(d3.format(" >8.4f")(e.alpha));
 fdgInst.text("fdg instance: " + instID);
};

d3.select("#update").on("click", (function() {
 var dataSet = false;
 return function() {
  //fdg.force.stop();
  fdg(dataSets[(dataSet = !dataSet, +dataSet)])
 }
})());
//////////////////////////////////////////////////////////////////////////////////////////
var dataSets = [{
    "nodes"    : [
     {"name": "node1", "r": 10},
     {"name": "node2", "r": 10},
     {"name": "node3", "r": 30},
     {"name": "node4", "r": 15}
    ],
    "edges": [
     {"source": 2, "target": 0},
     {"source": 2, "target": 1},
     {"source": 2, "target": 3}
    ]
   },
   {
    "nodes":[
     {"name": "node1", "r": 20},
     {"name": "node2", "r": 10},
     {"name": "node3", "r": 30},
     {"name": "node4", "r": 15},
     {"name": "node5", "r": 10},
     {"name": "node6", "r": 10}
    ],
    "edges":[
     {"source": 2, "target": 0},
     {"source": 2, "target": 1},
     {"source": 2, "target": 3},
     {"source": 2, "target": 4},
     {"source": 2, "target": 5}
    ]
   }
  ],
  svg = SVG({width: 600, height: 200-34, margin: {top: 25, right: 5, bottom: 5, left: 5}}, "#viz"),
  fdg = FDG(svg, alpha.log);

fdg(dataSets[0]);

function SVG (size, selector){
 //delivers an svg background with zoom/drag context in the selector element
 //if height or width is NaN, assume it is a valid length but ignore margin
 var margin = size.margin || {top: 0, right: 0, bottom: 0, left: 0},
   unitW = isNaN(size.width), unitH = isNaN(size.height),
   w = unitW ? size.width : size.width - margin.left - margin.right,
   h = unitH ? size.height : size.height - margin.top - margin.bottom,
   zoomed = function(){return this},

   zoom = d3.behavior.zoom().scaleExtent([0.4, 4])
    .on("zoom", function(d, i, j){
     zoomed.call(this, d, i, j);
    }),

   svg = d3.select(selector).selectAll("svg").data([["transform root"]]);
   svg.enter().append("svg");
   svg.attr({width: size.width, height: size.height});

 var g = svg.selectAll("#zoom").data(id),
   gEnter = g.enter().append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .call(zoom)
    .attr({class: "outline", id: "zoom"}),
   zoomText = gEnter.append("text")
    .text("transform = translate ( margin.left , margin.top )")
    .style("fill", "#5c5c5c")
    .attr("dy", "-.35em"),
   surface = gEnter.append("rect")
    .attr({width: w, height: h})
    .style({"pointer-events": "all", fill: "#ccc", "stroke-width": 3, "stroke": "#fff"}),
   surfaceText = gEnter.append("text")
    .text("pointer-events: none")
    .style("fill", "#5c5c5c")
    .attr({"dy": "1em", "dx": ".2em"});

 g.h = h;
 g.w = w;
 g.onZoom = function(cb){zoomed = cb;};

 return g;
}
function FDG (svg, tickLog) {
 var instID = Date.now();
 force = d3.layout.force()
  .size([svg.w, svg.h])
  .charge(-1000)
  .linkDistance(50)
  .on("end", function(){
   // manage dead instances of force
   // only stop if this instance is the current owner
   if(cog.datum().instID != instID) return true;
   cog.classed("fa-spin", false);
   elapsedTime.stop();
  })
  .on("start", function(){
   // mark as active and brand the insID to establish ownership
   cog.classed("fa-spin", true).datum().instID = instID;
   elapsedTime.start();
  });

 function fdg(data) {
    force
     .nodes(data.nodes)
     .links(data.edges)
     .on("tick", (function(instID) {
      return function(e) {
       if(tickLog) tickLog.call(this, e, instID);
       lines.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("transform", function(d) {
        return "translate(" + [d.x, d.y] + ")"
       });
      }
     })(instID))
     .start();

  svg.onZoom(zoomed);

  hookDrag(force.drag(), "dragstart.force", function(d) {
   // prevent dragging on the nodes from dragging the canvas
   var e = d3.event.sourceEvent;
   e.stopPropagation();
   d.fixed = e.shiftKey || e.touches && (e.touches.length > 1);
  });
  hookDrag(force.drag(), "dragend.force", function(d) {
   // prevent dragging on the nodes from dragging the canvas
   var e = d3.event.sourceEvent;
   d.fixed = e.shiftKey || d.fixed;
  });

  var content = svg.selectAll("g#fdg").data([data]);
  content.enter().append("g").attr({"id": "fdg", class: "outline"});

  var contentText = content.selectAll(".contentText")
   .data(["transform = translate ( d3.event.translate ) scale ( d3.event.scale )"])
   .enter().append("text").classed("contentText", true)
   .text(id)
   .style("fill", "#5c5c5c")
   .attr({"dy": 20, "dx": 20});

  var lines = content.selectAll(".links")
     .data(linksData),
    linesEnter = lines.enter()
     .insert("line", d3.select("#nodes") ? "#nodes" : null)
     .attr("class", "links")
     .attr({stroke: "steelblue", "stroke-width": 3});
  var nodes = content.selectAll("#nodes")
     .data(nodesData),
    nodesEnter = nodes.enter().append("g")
     .attr("id", "nodes"),
    node = nodes.selectAll(".node")
     .data(id),
    newNode = node.enter().append("g")
     .attr("class", "node")
     .call(force.drag),
    circles = newNode.append("circle")
     .attr({class: "content"})
     .attr("r", function(d) {return d.r})
     .style({"fill": "red", opacity: 0.8});

  lines.exit().remove();
  node.exit().remove();

  function nodesData(d) {
   return [d.nodes];
  }

  function linksData(d) {
   return d.edges;
  }

  function hookDrag(target, event, hook) {
   //hook force.drag behaviour
   var stdDragStart = target.on(event);
   target.on(event, function(d) {
    hook.call(this, d);
    stdDragStart.call(this, d);
   });
  }

  function zoomed(){
   var e = d3.event.sourceEvent,
     isWheel = e && ((e.type == "mousewheel") || (e.type == "wheel"));
   force.alpha(0.01);
   return isWheel ? zoomWheel.call(this) : zoomInst.call(this)
  }
  function zoomInst(){
   var t = d3.transform(content.attr("transform"));
   t.translate = d3.event.translate; t.scale = d3.event.scale;
   content.attr("transform", t.toString());
  }
  function zoomWheel(){
   var t = d3.transform(content.attr("transform"));
   t.translate = d3.event.translate; t.scale = d3.event.scale;
   content.transition().duration(450).attr("transform", t.toString());
  }

  fdg.force = force;

 };
 return fdg

}
function id(d){return d;}
svg {
      outline: 1px solid #282f51;
      pointer-events: all;
      overflow: visible;
    }

    g.outline {
      outline: 1px solid red;
    }

    #panel div {
      display: inline-block;
      margin: 0 .25em 3px 0; 
      
    }
    #panel div div {
      white-space: pre;
    }
    div#inputDiv {
      white-space: normal;
      display: inline-block;
    }

    .node {
      cursor: default;
    }

    text {
      font-size: 8px;
    }
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/40e550b1507cca31b0bb/raw/b83ceb0f8b4a2b6256f079f5887fc5243baedd4f/elapsed%2520time%25201.0.js"></script>
<div id="panel">
  <div id="inputDiv">
    <input id="update" type="button" value="update">
  </div>
  <div id="wrapAlpha">alpha:
    <div id="alpha"></div>
  </div>
  <div id="fdg">
</div>
<div id="viz"></div>