Skip to content

Instantly share code, notes, and snippets.

@cool-Blue
Last active September 28, 2015 15:58
Show Gist options
  • Save cool-Blue/d7cc880054fcee402ace to your computer and use it in GitHub Desktop.
Save cool-Blue/d7cc880054fcee402ace to your computer and use it in GitHub Desktop.
d3 force layout inertial drag
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>SO30387784</title>
<style>
div {
display: inline-block;
}
div#panel {
display: block;
white-space: normal;
background-color: #ccc;
padding: 0 3px 0 3px;
}
svg {
display: block;
overflow: visible;
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.min.css">
</head>
<body>
<div id="panel">
<div id="wrapAlpha">alpha:
<div id="alpha"></div>
</div><div id="fdg">
</div>
</div>
<div id="viz"></div> <!--<script src="jquery-1.11.1.js"></script>-->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<!--<script src="d3 CB.js"></script>-->
<!--<script src="elapsedTime/elapsedTime/elapsed time 2.0.js"></script>-->
<script src="https://gitcdn.xyz/repo/cool-Blue/d3/master/d3%20CB.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/elapsedTime/elapsed-time-2.0.js"></script>
<script src="script.js"></script>
</body>
</html>
(function (d3) {
//debug panel/////////////////////////////////////////////////////////////////////////////
var panel = d3.select("#panel"),
logEvents = outputs.OutputDiv("#panel", {"background-color": "#ccc", margin: 0}, "#wrapAlpha");
logEvents.message(function (d) {
var width = 12, pad = Array(width+1).join(" ");
return ((d3.event ? d3.event.type : d)+pad).slice(0,width);
});
logEvents.update("waiting...")
var alpha = d3.select("#alpha"),
cog = d3.select("#wrapAlpha")
.style({
"background-color": "#ccc",
margin : 0,
padding : '3px 3px 3px 3px',
display : 'inline-block',
})
.insert("i", "#fdg")
.classed("fa fa-cog fa-spin", true)
.datum({instID: null}),
elapsedTime = outputs.ElapsedTime("#panel", {margin: 0, "background-color": "#ccc"})
.message(function (id) {
return 'fps : ' + d3.format(" >8.3f")(1/this.aveLap())
});
elapsedTime.consoleOn = false;
var forceState = outputs.OutputDiv("#panel", {margin: 0, "background-color": "#ccc"})
.message(function () {
return ' gravity: ' + d3.format(".3f")(g.force.gravity())
+ '\tcharge: ' + d3.format(".1f")(g.force.charge())
+ '\tfriction: ' + d3.format(".3f")(g.force.friction())
+ "\t velocity:\t" +( !isNaN(g.v().dx) ? (d3.format(">8.3f")(g.v().dx) + "\t" + d3.format(">8.3f")(g.v().dy)) : "")
});
alpha.log = function(e, force) {
elapsedTime.mark().timestamp();
alpha.text(d3.format(" >8.4f")(e.alpha));
forceState.update(force)
};
//////////////////////////////////////////////////////////////////////////////////////////
var width = $(window).width(),
height = $(window).height()*0.9;
var circles = [
{
x: width / 2 + 100,
y: height / 2,
radius: 50
},
{
x: width / 2 - 100,
y: height / 2,
radius: 100
},
// {
// x: width / 2,
// y: height / 2 + 100,
// radius: 100
// },
//{
// x: width / 2 + 100,
// y: height / 2,
// radius: 100
//},
//{
// x: width / 2 - 100,
// y: height / 2,
// radius: 100
//},
//{
// x: width / 2,
// y: height / 2 + 100,
// radius: 100
//},
],
collide = Collide(circles, 0),
nodeFill = "#006E3C";
//panel.attr("width", 50 + "%");
var force = d3.layout.force()
.gravity(0)
.charge(0)
.friction(1)
.size([width, height])
.nodes(circles)
.linkDistance(250)
.linkStrength(1)
.on("tick", tick)
.on("start", function () {
elapsedTime.start()
})
.start();
circles.forEach(function(d) {
// add velocity interface
d.v = {
get x() {return d.x - d.px},
get y() {return d.y - d.py},
get v() {return [this.x, this.y];},
set v(vel) {d.px = d.x - vel[0]; d.py = d.y - vel[1]},
get m() {return Math.sqrt(this.x*this.x + this.y*this.y)}
};
// add a quantised version of all numeric properties
d.q = {};
Object.keys(d).forEach(function(p){
if(!isNaN(d[p])) Object.defineProperty(d.q, p, {
get: function () {return Math.round(d[p])}
});
})
// set random initial velocities
d.v.v = [Math.random()*30, Math.random()*30]
});
var svg = d3.select("#viz")
.append("svg")
.attr("width", width)
.attr("height", height)
.style({"background-color": "black", opacity: 0.6});
var nodes = svg.selectAll(".node");
nodes = nodes.data(circles);
nodes.exit().remove();
var enterNode = nodes.enter().append("g")
.attr("class", "node")
.call(force.drag);
//Add circle to group
enterNode.append("circle")
.attr("r", function (d) {
return d.radius;
})
.style("fill", "#006E3C")
.style("opacity", 0.9);
//Drag behaviour///////////////////////////////////////////////////////////////////
// hook drag behavior on force
//VELOCITY
// maintain velocity state in case a force tick occurs immediately before dragend
// the tick wipes out previous position
var dragVelocity = (function () {
var dx, dy, dd;
function f(d) {
dd = d = d || dd;
if (d3.event) {
dx = d3.event.dx; dy = d3.event.dy;
return { dx: dx, dy: dy }
} else {
if (d)
return { dx: d.v.x, dy: d.v.y };
else
return { dx: "...vx", dy: "...vy" };
}
};
f.correct = function (d) {
dd = d = d || dd;
if (true && dx && d.x === d.px && d.y === d.py) {
//tick occurred and set px/y to x/y, re-establish velocity state
d.px = d.x - dx; d.py = d.y - dy;
} else {
//not used!
//velocity state is ok but previous and current are reversed during drag
//correct the velocity direction
x = d.x; d.x = d.px; d.px = x;
y = d.y; d.y = d.py; d.py = y;
}
}
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);
logEvents.update();
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", myMouseOver);
d.___mouseout_force = node.on("mouseout.force");
d.___hooked = true;
//standard mouseout will clear d.fixed
d.___mouseout_force.call(that, d);
}
//disable mouseout/////////////////////////
node.on("mouseout.force", null);
}
function myMouseOver(d) {
logEvents.update();
}
}
var f = d3.format("6,.0f");
//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);
//var x = d.x, y = d.y;
//stop dead
//d.px = d.x; d.py = d.y;
//correct the final velocity vector at drag end
dragVelocity.correct(d)
logEvents.update();
//hook mouseout/////////////////////////
//re-establish standard behavior on mouseout
node.on("mouseout.force", function mouseout(d) {
myForceMouseOut.call(this, d, elapsedTime.t() + "\tmouseout")
});
stdDragEnd.call(that, d);
function myForceMouseOut(d, timeSet) {
var timerID = window.setTimeout((function () {
var that = this, node = d3.select(this);
return function unhookMouseover() {
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), 1000);
return timerID;
}
}
//DYNAMICS/////////////////////////
function tick(e) {
if(alpha && alpha.log) alpha.log.call(this, e);
nodes.attr("transform", function (d) {
var r = d.radius;
if (d.x - r <= 0 && d.q.px >= d.q.x) boundary(d, "x", [0, width]);
if (d.x + r >= width && d.q.px <= d.q.x) boundary(d, "x", [width, 0]);
if (d.y - r <= 0 && d.q.py >= d.q.y) boundary(d, "y", [0, height]);
if (d.y + r >= height && d.q.py <= d.q.y) boundary(d, "y",[height, 0]);
collide(e.alpha, r)(d);
return "translate(" + d.x + "," + d.y + ")";
function boundary(p, y, b) {
var k;
if(p.q[y] === p.q["p"+y]) {
// node velocity is zero
p[y] += ((b[0] < b[1]) ? 1 : -1);
}else {
p["p" + y] = 2* p[y] - p["p" + y];
}
}
});
nodes.selectAll("circle").style("fill", function (d, i) { return ((d.___hooked && !d.fixed) ? "red" : nodeFill) })
nodes.each(function(d, i) { if(d.___hooked && !d.fixed) console.log(i + "\thooked but not fixed")})
force.start();
}
function Collide(nodes, padding) {
// Resolve collisions between nodes.
var maxRadius = d3.max(nodes, function(d) {return d.radius});
return function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
var possible = !(x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1);
if (quad.point && (quad.point !== d) && possible) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + padding,
m = Math.pow(quad.point.radius, 3),
mq = Math.pow(d.radius, 3),
mT = m + mq;
if (l < r) {
//move the nodes away from each other along the radial (normal) vector
//taking relative mass into consideration, the sign is already established
//in calculating x and y and the nodes are modelled as spheres for calculating mass
l = (r - l) / l * alpha;
d.x += (x *= l) * m/mT;
d.y += (y *= l) * m/mT;
quad.point.x -= x * mq/mT;
quad.point.y -= y * mq/mT;
}
}
return !possible;
});
};
}
}
var g = {};
g.force = force;
g.v = dragVelocity;
})(d3);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment