Skip to content

Instantly share code, notes, and snippets.

@plmrry
Last active August 29, 2015 14:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save plmrry/2a747d5ec441121cda76 to your computer and use it in GitHub Desktop.
Save plmrry/2a747d5ec441121cda76 to your computer and use it in GitHub Desktop.
Force-Directed Control Point Interpolation

A technique for interpolating the positioning of nodes between a force-directed layout and a set of pre-set control points.

Based on Grid Experiments by Moritz Stefaner, via Jim Vallandingham's talk on "Using and Abusing the Force."

<!doctype html>
<html>
<head>
<meta name="description" content="Force matrix positions" />
<title>MatrixVis</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link href='//fonts.googleapis.com/css?family=Roboto:400,100,200,700' rel='stylesheet' type='text/css'>
<style>
@media (min-width: 1600px) {
.container {
width: 1500px;
}
}
@media (min-width: 900px) {
.container {
width: 800px;
}
}
</style>
</head>
<body>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js" charset="utf-8"></script>
<script src="//code.jquery.com/jquery-latest.min.js"></script>
<script>
var d3 = window.d3;
var dispatch = d3.dispatch("resize", "update");
dispatch.on("resize.main", redraw);
$(window).resize( dispatch.resize );
var figure = d3.select("body")
.append("main").classed("container", true)
.append("figure").classed("row", true);
var svgStyle = { border: "1px solid #888" }; /** FIXME: Dev only. Outline <svg> elements. */
var networkDiag = diagram("network", "col-xs-12", 0.5, svgStyle);
var controlsDiag = diagram("controls", "col-xs-12", 0.1, svgStyle);
figure.call(networkDiag).call(controlsDiag);
$(window).resize();
var color = d3.scale.category10();
d3.select("svg.network").each(network);
d3.select("svg.controls").each(controls);
function controls() {
var div = d3.select( this.parentNode );
div.select("svg.controls").remove();
var s;
var slider = div.append("input").attr("id", "theta")
.property({ type: "range", min: 0, max: 1, step: 0.01, value: 0 })
.each(function() {
s = d3.scale.linear().domain([0, this.clientWidth])
.range([this.min, this.max]).clamp(true);
})
.on("mousemove", function() {
var val = s(d3.mouse(this)[0]);
d3.select(this).property("value", val);
text.text(parseFloat(val).toFixed(2));
dispatch.update(val);
})
var text = div.append("text").attr({ x: 10, y: 100 }).text("foo");
}
function network() {
var width = 1000, margin = 50,
boil = boilerplate(this, width, margin),
g = boil.g, size = boil.size;
var rect = g.append("rect")
.attr({ width: size.width, height: size.height })
/** FIXME: rect styling for dev only */
.style({ fill: "#fff", stroke: "#ccc", "stroke-width": "2px" });
var groups = 4,
nodesPer = 15,
p = 0.001;
var nodeArray = makeNodes(groups, nodesPer);
var linkArray = makeLinks(nodeArray, p);
// console.log(size);
var force = d3.layout.force()
.nodes(nodeArray)
.links(linkArray)
.size([size.width / 2, size.height])
// .linkDistance(50).linkStrength(0.01)
.start()
.on("tick", tick);
// console.log(force.nodes())
var t = d3.range(groups * nodesPer).map(function() { return {}; });
var targets = g.selectAll(".target").data(t)
.enter()
.append("circle")
.classed("target", true).attr("r", 2).attr("fill-opacity", 0.5)
var links = g.selectAll(".link").data(force.links())
.enter().append("line")
.classed("link", true)
.style({
stroke: "#555"
});
var nodes = g.selectAll(".node").data(force.nodes())
.enter().append("circle")
.classed("node", true)
.attr("r", 6)
.style("fill", function(d) { return color(d.group) })
.call(force.drag);
var text = g.append("text")
.attr({x: 50, y: 50})
.style("font-size", "30px");
var base = { x: 0, y: 0 };
var s = d3.scale.ordinal().domain(d3.range(targets.size())).rangePoints([0, size.height], 5);
var old;
rect.on("mousemove", function() {
var m = d3.mouse(this);
base.x = m[0]; base.y = m[1];
targets.each(function(d, i) {
d.x = base.x + 50;
d.y = base.y - 100 + s(i);
})
force.start();
}).on("click", function() {
if (rect.on("mousemove")) {
old = rect.on("mousemove");
rect.on("mousemove", null);
} else {
rect.on("mousemove", old);
}
})
var value = 0, str = force.linkStrength();
dispatch.on("update.network", function(v) {
value = d3.scale.pow().exponent(4)(v);
force.linkStrength(str * (1 - value));
force.start();
})
function tick(e) {
targets.each(function(t) { t.occupied = false; });
nodes.each(function(d, nodeI) {
/** How to move nodes:
* Directly: d.x += (pos.x - d.x);
* Blended: d.x += (pox.x - d.x) * e.alpha;
*/
/** Based heavily on github.com/MoritzStefaner/gridexperiments/ */
var minDist = 1e6, dist, candidate = null;
targets.each(function(t, targetI) {
dist = distance(d, t)
if ( ( ! t.occupied ) && dist < minDist) {
minDist = dist;
candidate = t;
}
})
if (candidate) {
candidate.occupied = true;
d.x += (candidate.x - d.x) * value * e.alpha * 10;
d.y += (candidate.y - d.y) * value * e.alpha * 10;
}
})
// text.text(e.alpha);
targets.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
nodes.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
links.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; });
}
}
function distance(a, b) { return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2); }
function makeNodes(g, n) {
var nodeArray = [];
d3.range(g).forEach(function(group) {
d3.range(n).forEach(function(node) {
nodeArray.push({ id: node + group * n, group: group });
});
});
return nodeArray;
}
function makeLinks(nodeArray, p) {
var linkArray = [], oldLength;
nodeArray = d3.shuffle(nodeArray);
nodeArray.forEach(connectNodes);
function connectNodes(source, i, nodeArray) {
oldLength = linkArray.length;
while (linkArray.length === oldLength) {
nodeArray.forEach(maybeAddLink(source));
}
}
function maybeAddLink(source) {
var chance;
return function(target) {
if (source.group === target.group) {
chance = p * 100;
} else {
chance = p * 10;
}
if (Math.random() < chance && source.id !== target.id) {
linkArray.push({ source: source, target: target });
}
};
}
return linkArray;
}
/**
* Redraw callback, to be called on window resize.
* Each <svg> should have a data-height-ratio,
* which is used to sets the <svg> element's
* height relative to the current window height.
*/
function redraw() {
figure.selectAll(".diagram")
.select("svg")
.each(function() {
var parentWidth = $(this.parentNode).width();
d3.select(this)
.style({
height: function() { return $(this).data("height-ratio") * parentWidth },
width: function() { return parentWidth; }
});
});
}
// Helper Functions.
///////////////////////////////////////////////////////////////////
function boilerplate(node, width, margin) {
var viewBox = { width: width + 2 * margin };
viewBox.height = viewBox.width * node.dataset.heightRatio;
var g = d3.select(node)
.attr({
viewBox: "0 0 " + viewBox.width + " " + viewBox.height,
preserveAspectRatio: "xMinYMin"
})
.append("g")
.attr("transform", translate(margin, margin ) );
var height = viewBox.height - 2 * margin;
var size = { width: width, height: height }
return { g: g, size: size };
}
function diagram(name, bootstrap, heightRatio, style) {
return function(s) {
s.append(div("diagram " + bootstrap))
.append("svg").classed(name, true)
.attr("data-height-ratio", heightRatio)
.style(style);
};
}
function div(clas) {
return function() { return $("<div>").addClass(clas).get(0); };
}
function translate(x, y) { return "translate(" + x + ", " + y + ")"; }
</script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment