问题
I'm using lastest d3js with force layout to create interactive graph like this:
Requirements are:
- Nodes can be dragged (inertial drag)
- Node bounces back when hitting border.
- Nodes don't overlap each other (I can do this based on Collision Detection sample)
Somebody please help me with 1 & 2.
The background for this question is in this related question.
Thank you.
回答1:
Background
The background for this answer is in my answer to a related question here.
That question was about why nodes jump back after release and the main point to bring from there is that, during force.drag behavior the previous node positions, (d.px, d.py) and the current positions (d.x, d.y) are actually reversed. So, when the drag is released, the initial velocity is therefor reversed, causing the jump back behavior.
This is actually due to the drag behavior updating the previous position on drag events and the internal force.tick method copying the previous values onto the current values each position calculation. (I'm sure there is a good reason for this by the way, I suspect it is related to this...)
Inertial dragging
In order to implement inertial dragging, this velocity reversal needs to be corrected, so the current and previous points need to be reversed immediately after dragend.
This is a good start, but a couple of other problems remain:
- Velocity state is lost every tick when previous position is copied to the current position.
- "Sticky node" behavior (on
mouseover) is re-established ondragendwhich tends to re-capture the nodes and defeat the inertial effect.
The first one means that if a tick occurs between releasing the drag and correcting the velocity, i.e. immediately after dragend, then the velocity will be zero and the node will stop dead. This happens often enough to be annoying. One solution is to maintain a record of d3.event.dx and d3.event.dy and use these to modify (d.px, d.py) on dragend. This also avoids the problem caused by the reversal of previous and current points.
The second remaining problem can be fixed by delaying the sticky node behavior reinstatement until after mouseout. A small delay after mouseout is advisable in case the mouse immediately re-enters the node after mouseout.
Implimentation
The basic strategy to implement the above two corrections is to hook the drag events in the force layout in the former and the mouse events in the force layout in the later. For defensive reasons, the standard callbacks for the various hooks are stored on the datum object of the nodes and retrieved from there when un-hooking.
The friction parameter is set to 1 in the code which means they maintain their velocity indefinitely, to see a steady inertial effect set it to 0.9... I jest like the bouncing balls.
$(function() {
var width = 1200,
height = 800;
var circles = [{
x: width / 2 + 100,
y: height / 2,
radius: 100
}, {
x: width / 2 - 100,
y: height / 2,
radius: 100
}, ],
nodeFill = "#006E3C";
var force = d3.layout.force()
.gravity(0)
.charge(-100)
.friction(1)
.size([width, height])
.nodes(circles)
.linkDistance(250)
.linkStrength(1)
.on("tick", tick)
.start();
SliderControl("#frictionSlider", "friction", force.friction, [0, 1], ",.3f");
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.style("background-color", "white");
var nodes = svg.selectAll(".node");
nodes = nodes.data(circles);
nodes.exit().remove();
var enterNode = nodes.enter().append("g")
.attr("class", "node")
.call(force.drag);
console.log(enterNode);
//Add circle to group
enterNode.append("circle")
.attr("r", function(d) {
return d.radius;
})
.style("fill", "#006E3C")
.style("opacity", 0.6);
;
(function(d3, force) {
//Drag behaviour///////////////////////////////////////////////////////////////////
// hook drag behavior on force
//VELOCITY
// maintain velocity state in case a force tick occurs emidiately before dragend
// the tick wipes out the previous position
var dragVelocity = (function() {
var dx, dy;
function f(d) {
if (d3.event) {
dx = d3.event.dx;
dy = d3.event.dy;
}
return {
dx: dx,
dy: dy
}
};
f.correct = function(d) {
//tick occured and set px/y to x/y, re-establish velocity state
d.px = d.x - dx;
d.py = d.y - dy;
}
f.reset = function() {
dx = dy = 0
}
return f;
})()
//DRAGSTART HOOK
var stdDragStart = force.drag().on("dragstart.force");
force.drag().on("dragstart.force", myDragStart);
function myDragStart(d) {
var that = this,
node = d3.select(this);
nonStickyMouse();
dragVelocity.reset();
stdDragStart.call(this, d)
function nonStickyMouse() {
if (!d.___hooked) {
//node is not hooked
//hook mouseover/////////////////////////
//remove sticky node on mouseover behavior and save listeners
d.___mouseover_force = node.on("mouseover.force");
node.on("mouseover.force", null);
d.___mouseout_force = node.on("mouseout.force");
d.___hooked = true;
//standard mouseout will clear d.fixed
d.___mouseout_force.call(that, d);
}
//dissable mouseout/////////////////////////
node.on("mouseout.force", null);
}
}
//DRAG HOOK
var stdDrag = force.drag().on("drag.force");
force.drag().on("drag.force", myDrag);
function myDrag(d) {
var v, p;
//maintain back-up velocity state
v = dragVelocity();
p = {
x: d3.event.x,
y: d3.event.y
};
stdDrag.call(this, d)
}
//DRAGEND HOOK
var stdDragEnd = force.drag().on("dragend.force");
force.drag().on("dragend.force", myDragEnd);
function myDragEnd(d) {
var that = this,
node = d3.select(this);
//correct the final velocity vector at drag end
dragVelocity.correct(d)
//hook mouseout/////////////////////////
//re-establish standard behavior on mouseout
node.on("mouseout.force", function mouseout(d) {
myForceMouseOut.call(this, d)
});
stdDragEnd.call(that, d);
function myForceMouseOut(d) {
var timerID = window.setTimeout((function() {
var that = this,
node = d3.select(this);
return function unhookMouseover() {
//if (node.on("mouseover.force") != d.___mouseout_force) {
if (node.datum().___hooked) {
//un-hook mouseover and mouseout////////////
node.on("mouseout.force", d.___mouseout_force);
node.on("mouseover.force", d.___mouseover_force);
node.datum().___hooked = false;
}
}
}).call(this), 500);
return timerID;
}
}
})(d3, force);
function tick(e) {
//contain the nodes...
nodes.attr("transform", function(d) {
var r = 100;
if (d.x - r <= 0 && d.px > d.x) d.px -= (d.px - d.x) * 2;
if (d.x + r >= width && d.px < d.x) d.px += (d.x - d.px) * 2;
if (d.y - r <= 0 && d.py > d.y) d.py -= (d.py - d.y) * 2;
if (d.y + r >= height && d.py < d.y) d.py += (d.y - d.py) * 2;
return "translate(" + d.x + "," + d.y + ")";
});
//indicate status by color
nodes.selectAll("circle")
.style("fill", function(d, i) {
return ((d.___hooked && !d.fixed) ? "red" : nodeFill)
})
force.start();
}
function SliderControl(selector, title, value, domain, format) {
var accessor = d3.functor(value),
rangeMax = 1000,
_scale = d3.scale.linear().domain(domain).range([0, rangeMax]),
_$outputDiv = $("<div />", {
class: "slider-value"
}),
_update = function(value) {
_$outputDiv.css("left", 'calc( ' + (_$slider.position().left + _$slider.outerWidth()) + 'px + 1em )')
_$outputDiv.text(d3.format(format)(value));
$(".input").width(_$outputDiv.position().left + _$outputDiv.outerWidth() - _innerLeft)
},
_$slider = $(selector).slider({
value: _scale(accessor()),
max: rangeMax,
slide: function(e, ui) {
_update(_scale.invert(ui.value));
accessor(_scale.invert(ui.value)).start();
}
}),
_$wrapper = _$slider.wrap("<div class='input'></div>")
.before($("<div />").text(title + ":"))
.after(_$outputDiv).parent(),
_innerLeft = _$wrapper.children().first().position().left;
_update(_scale.invert($(selector).slider("value")))
};
});
body {
/*font-family: 'Open Sans', sans-serif;*/
font-family: 'Roboto', sans-serif;
}
svg {
outline: 1px solid black;
background-color: rgba(255, 127, 80, 0.6);
}
div {
display: inline-block;
}
#method,
#clear {
margin-left: 20px;
background-color: rgba(255, 127, 80, 0.6);
border: none;
}
#clear {
float: right;
}
#inputs {
font-size: 16px;
display: block;
width: 900px;
}
.input {
display: inline-block;
background-color: rgba(255, 127, 80, 0.37);
outline: 1px solid black;
position: relative;
margin: 10px 10px 0 0;
padding: 3px 10px;
}
.input div {
width: 60px;
}
.method {
display: block;
}
.ui-slider,
span.ui-slider-handle.ui-state-default {
width: 3px;
background: black;
border-radius: 0;
}
span.ui-slider-handle.ui-state-default {
top: calc(50% - 1em / 2);
height: 1em;
margin: 0;
border: none;
}
div.ui-slider-horizontal {
width: 200px;
margin: auto 10px auto 10px;
/*position: absolute;*/
/*bottom: 0.1em;*/
position: absolute;
bottom: calc(50% - 2.5px);
/*vertical-align: middle;*/
height: 5px;
border: none;
}
.slider-value {
position: absolute;
text-align: right;
}
input,
select,
button {
font-family: inherit;
font-size: inherit;
}
<link href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css" rel="stylesheet" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="inputs">
<div id="frictionSlider"></div>
</div>
来源:https://stackoverflow.com/questions/30480313/inertial-drag-in-force-layout