Force Layout Drag Behaviour With Transition On Zoom But No Transition On Drag
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 ab
Solution 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]});
});
functionzoomStart(){
}
functionzoomed(){
return d3.event.sourceEvent.buttons ? zoomDrag.call(this) : zoomScale.call(this)
}
functionzoomDrag(){
var t = d3.transform(content.attr("transform"));
t.translate = d3.event.translate;
content.attr("transform", t.toString());
}
functionzoomScale(){
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";
}
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script><svgid="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 the
force.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 the
force.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 behaviourvar 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;
returnfunction() {
//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]);
functionSVG (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 marginvar 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(){returnthis},
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;
}
functionFDG (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 ownerif(cog.datum().instID != instID) returntrue;
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();
});
functionfdg(data) {
force
.nodes(data.nodes)
.links(data.edges)
.on("tick", (function(instID) {
returnfunction(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 canvasvar 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 canvasvar 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();
functionnodesData(d) {
return [d.nodes];
}
functionlinksData(d) {
return d.edges;
}
functionhookDrag(target, event, hook) {
//hook force.drag behaviourvar stdDragStart = target.on(event);
target.on(event, function(d) {
hook.call(this, d);
stdDragStart.call(this, d);
});
}
functionzoomed(){
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)
}
functionzoomInst(){
var t = d3.transform(content.attr("transform"));
t.translate = d3.event.translate; t.scale = d3.event.scale;
content.attr("transform", t.toString());
}
functionzoomWheel(){
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
}
functionid(d){return d;}
svg {
outline: 1px solid #282f51;
pointer-events: all;
overflow: visible;
}
g.outline {
outline: 1px solid red;
}
#paneldiv {
display: inline-block;
margin: 0 .25em3px0;
}
#paneldivdiv {
white-space: pre;
}
div#inputDiv {
white-space: normal;
display: inline-block;
}
.node {
cursor: default;
}
text {
font-size: 8px;
}
<linkrel="stylesheet"href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.min.css"><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script><scriptsrc="https://gitcdn.xyz/repo/cool-Blue/40e550b1507cca31b0bb/raw/b83ceb0f8b4a2b6256f079f5887fc5243baedd4f/elapsed%2520time%25201.0.js"></script><divid="panel"><divid="inputDiv"><inputid="update"type="button"value="update"></div><divid="wrapAlpha">alpha:
<divid="alpha"></div></div><divid="fdg"></div><divid="viz"></div>
Post a Comment for "Force Layout Drag Behaviour With Transition On Zoom But No Transition On Drag"