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

安稳与你 提交于 2019-12-08 12:18:25

问题


I think the question is very easy. I have this site for you for demontrating what I mean: http://arda-maps.org/familytree/ So if you add some persons there to the screen you are able to drag and zoom the view.

The zooming is totally fine. The duration for zooming is great. But I don't like the duration when dragging and would like to disable it just there. Basically this is the code:

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

So the question is how to disable the transition/duration just on the dragging event? Is this even possible somehow?


回答1:


Phase I

I think this is close... It just needs to be verified that it will play nice with the drag behaviour on the nodes.

Stategy

  • use d3.event.sourceEvent.type to check for mousemove
  • augment the current transform state using d3.transform
  • transition translate and scale for mouse wheel events and no transition for mouse button events

Working example

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>

Phase II

Incorporate an FDG

Since the FDG must be inside the canvas container, it's necessary to stop node level events from propagating to the canvas. This was done in the OP code by using a custom drag behaviour, stopping propagation on dragstart and adding back some of the force.drag behaviour (plus setting 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` behaviour.

Strategy

  • apply the same principles as in Phase I but do a cross-browser test for mouse wheel events.
  • add standard force.drag to the nodes
  • hook the force.drag to add custom behaviour
  • only fix nodes on shift-drag (or shift-dragend)
  • for touch devices, also fix nodes if touches > 1 at dragstart

The last two points allow fixed nodes to be easily released if desired.

force.drag hook

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

Working example

//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>


来源:https://stackoverflow.com/questions/31469803/force-layout-drag-behaviour-with-transition-on-zoom-but-no-transition-on-drag

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!