Skip to content

Instantly share code, notes, and snippets.

@cool-Blue
Last active August 31, 2015 16:51
Show Gist options
  • Save cool-Blue/bd76db6e38915c30fc5b to your computer and use it in GitHub Desktop.
Save cool-Blue/bd76db6e38915c30fc5b to your computer and use it in GitHub Desktop.
self-sorting nodes in d3 fdg

Force Directed Graph with self sorting nodes - Inertial winnowing

Features

  • Collisions between nodes
    Based on this example but enhanced to simulate inertia. The distance each node is moved away from the collision is proportional to their relative mass. Since gravity is switched on and friction is set for low damping, the heavier nodes will move towards the center of the graph and the smaller nodes pushed out of the way. The mass is calculated assuming the nodes are spheres, using r3, and the rebounds calculated according to relative "mass".
  force.alpha(a/0.99*(1 - x))

d3 features used

  1. d3.layout.force
  2. Ordinal Scales
  3. d3.format
  4. d3.range
  5. d3.geom.quadtree
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
/*margin: 200px 500px 100px 500px;*/
}
#inputs {
display: inline-block;
margin: 0 0 0 0.5em;
}
#panel {
display: inline-block;
margin: 0 0 0 100px;
border: none;
box-sizing: border-box;
background-color: black;
}
#metrics {
display: inline-block;
}
label, input {
text-align: left;
width: 3.5em;
color: orange;
/*padding-left: 1em;*/
background-color: black;
outline: none;
border: none;
}
circle {
stroke: black;
}
svg {
display: block;
overflow: visible;
border: none;
background: black;
margin: 0 100px 0 100px;
}
text {
text-anchor: middle;
}
rect {
stroke: #ccc;
}
</style>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<!--<script src="d3 CB.js"></script>-->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/tinycolor/1.1.2/tinycolor.min.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/filters/shadow.js"></script>
<script
src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/elapsedTime/elapsed-time-1.0.js"></script>
<script>
var inputs = d3.select("body").append("div")
.attr("id", "metrics")
.attr("id", "panel")
.append("div").attr({id: "inputs"}),
nodeCount = inputs.append("label")
.attr("for", "nodeCount")
.text("nodes: ")
.append("input")
.attr({id : "nodeCount",
class : "numIn",
type : "number",
min : "100",
max : "5,000",
step : "100",
inputmode: "numeric"
});
var elapsedTime = ElapsedTime("#panel", {
border : 0, margin: 0, "box-sizing": "border-box",
padding: "0 0 0 3px", background: "black", "color": "orange"
})
.message(function(value) {
var this_lap = this.lap().lastLap, aveLap = this.aveLap(this_lap)
return 'alpha:' + d3.format(" >7,.3f")(value)
+ '\ttick time:' + d3.format(" >8,.4f")(this_lap)
+ ' (' + d3.format(" >4,.3f")(this.aveLap(this_lap)) + ')'
+ '\tframe rate:' + d3.format(" >4,.1f")(1 / aveLap) + " fps"
}),
width = 960 - 200,
height = 500 - elapsedTime.selection.node().clientHeight,
padding = 4, // separation between nodes
maxRadius = 7;
var n = 500, // total number of nodes
m = 1, // number of distinct layers
c = 10,
g = 0.2, g2 = 0.1,
f1 = 0.5, f2 = 0.01,
q2 = -40;
var tick = (function() {
var phase = -1, stage1 = true;
function tick(e) {
viz.circle.each(viz.collide(e.alpha * 40));
if(e.alpha < 0.02 || !(phase = ++phase % 4)) {
elapsedTime.mark(e.alpha);
viz.circle.attr({
cx: function(d) {
return d.x;
},
cy: function(d) {
return d.y;
}
});
}
if(stage1 && e.alpha < 0.03) {
console.log("stage2")
force.friction(f2)
.charge(q2)
.gravity(g2)
.start().alpha(e.alpha);
stage1 = false;
}
force.alpha(e.alpha / 0.99 * 0.998)
}
tick.reset = function() {
stage1 = true;
};
return tick;
})(),
force = d3.layout.force()
.size([width, height])
.gravity(g)
.charge(0)
.friction(f1)
.on("tick", tick)
.on("start", function() {
elapsedTime.start(1000);
force
.gravity(g)
.charge(0)
.friction(f1)
tick.reset();
});
force.drag().on("dragend", function(){force.alpha(0.05)})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g"),
bubble = Bubble(svg);
var viz = update(force, n, padding);
nodeCount
.property("value", n)
.on("change", function() {
viz = update(force, this.value, padding);
this.blur();
});
elapsedTime.selection.style({
width: (width
- parseFloat(window.getComputedStyle(d3.select("#inputs").node()).getPropertyValue("width"))
- parseFloat(window.getComputedStyle(d3.select("#inputs").node()).getPropertyValue("margin-left")))
+ "px"
});
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, 4),
mq = Math.pow(d.radius, 4),
mT = m + mq;
if(l < r) {
for(; Math.abs(l) == 0;) {
x = Math.round(Math.random() * r);
y = Math.round(Math.random() * r);
l = Math.sqrt(x * x + y * y);
}
//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 * (1 + 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;
});
};
}
}
function initNodes(force, n, padding) {
var rMax = Math.pow(500 / n * 50, 0.5);
force.stop()
.nodes(d3.range(n).map(function() {
var u = Math.random(),
v = -Math.log(u);
return {
radius : Math.pow(v, 0.8) * rMax,
color : Math.floor(u * c),
x : width / 2,
y : height / 2,
get v() {
var d = this;
return {x: d.x - d.px || d.x || 0, y: d.y - d.py || d.y || 0}
},
frustration: (function() {
//if they can't get home, they get angry, but, as soon as they're home, they're fine
var anger = 1;
return function() {
var d = this, anxious = (Math.abs(d.cy - d.y) > w.rangeBand()
/ 2);
return anger = anxious ? anger + windUp.value() : 1;
}
})()
}
}))
.start();
return Collide(force.nodes(), padding);
}
function update(force, n, padding) {
return {
collide: initNodes(force, n, padding),
circle : (function() {
var update = svg.selectAll("circle")
.data(force.nodes());
update.enter().append("circle");
update.exit().remove();
update.attr("r", function(d) {
return d.radius;
})
.call(bubble.call)
.call(force.drag)
return update;
})()
};
}
function Bubble(svg) {
var colors = d3.range(20).map(d3.scale.category10()).map(function(d) {
return filters.sphere(svg, d, 1)
});
return {
call: function(selection) {
selection.style("fill", function(d) {
return colors[d.color]
})
},
map : function(d, i, data) {
d.fill = colors[~~(Math.random() * 20)];
},
fill: function(d) {
return d.fill
}
}
}
;
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment