Skip to content

Instantly share code, notes, and snippets.

@mattkohl
Last active June 12, 2016 14:02
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 mattkohl/08fe7f53f592ab699ea15af46be04c48 to your computer and use it in GitHub Desktop.
Save mattkohl/08fe7f53f592ab699ea15af46be04c48 to your computer and use it in GitHub Desktop.
Force-Directed Graph with Reflexive Edges
license: gpl-3.0

This adaptation of Force-Directed Graph with Multiple Labelled Edges between Nodes shows reflexive edges (same source & target node). Support for these is as simple as adding a conditional to the function arcPath:

if (dr === 0) {
    sweep = 0;
    xRotation = -45;
    largeArc = 1;
    drx = 150;
    dry = 110;
    x2 = x2 + 1;
    y2 = y2 + 1;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Force Layout with multiple labelled edges between nodes</title>
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Open+Sans);
body {
background: #333333;
color: white;
}
text {
fill: white;
font-family: 'Open Sans';
}
circle {
stroke: white;
stroke-width: 1.5px;
}
path.link, path.textpath {
fill: none;
stroke: #cccccc;
stroke-width: 0.5px;
}
path.invis {
fill: none;
stroke-width: 0;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script type="text/javascript">
var data = {
edges: [
{
source: {id: 0, label: ""},
target: {id: 0, label: ""},
value: "Walk around in..."
},
{
source: {id: 0, label: ""},
target: {id: 0, label: ""},
value: "Walk around in circles"
},
{
source: {id: 0, label: ""},
target: {id: 0, label: ""},
value: "Walk around in circles,"
},
{
source: {id: 0, label: ""},
target: {id: 0, label: ""},
value: "walk around in circles"
},
{
source: {id: 0, label: ""},
target: {id: 0, label: ""},
value: "I don't need to"
},
]
};
function myGraph() {
this.addNode = function (n) {
if (!findNode(n.id)) {
nodes.push({"id": n.id, "label": n.label});
update();
}
};
this.addLink = function (source, target, value) {
links.push({"source": findNode(source.id), "target": findNode(target.id), "value": value});
update();
};
this.initialize = function() {
data.edges.forEach(function(d) {
graph.addNode(d.source);
graph.addNode(d.target);
graph.addLink(d.source, d.target, d.value);
});
};
var findNode = function (nodeId) {
for (var i in nodes) {
if (nodes[i].id === nodeId) {
return nodes[i];
}
};
};
var countSiblingLinks = function(source, target) {
var count = 0;
for(var i = 0; i < links.length; ++i){
if( (links[i].source.id == source.id && links[i].target.id == target.id) || (links[i].source.id == target.id && links[i].target.id == source.id) )
count++;
};
return count;
};
var getSiblingLinks = function(source, target) {
var siblings = [];
for(var i = 0; i < links.length; ++i){
if( (links[i].source.id == source.id && links[i].target.id == target.id) || (links[i].source.id == target.id && links[i].target.id == source.id) )
siblings.push(links[i].value);
};
return siblings;
};
var w = window.innerWidth - 20,
h = window.innerHeight,
middle = w/2;
var linkDistance = 400;
var colors = d3.scale.category20();
var svg = d3.select("body")
.append("svg:svg")
.attr("width", w)
.attr("height", h)
.style("z-index", -10)
.attr("id", "svg");
var force = d3.layout.force();
var nodes = force.nodes(),
links = force.links();
var update = function () {
var path = svg.selectAll("path.link")
.data(force.links());
path.enter().append("svg:path")
.attr("id", function (d) {
return d.source.id + "-" + d.value + "-" + d.target.id;
})
.attr("class", "link");
path.exit().remove();
var pathInvis = svg.selectAll("path.invis")
.data(force.links());
pathInvis.enter().append("svg:path")
.attr("id", function (d) {
return "invis_" + d.source.id + "-" + d.value + "-" + d.target.id;
})
.attr("class", "invis");
pathInvis.exit().remove();
var pathLabel = svg.selectAll(".pathLabel")
.data(force.links());
pathLabel.enter().append("g").append("svg:text")
.attr("class", "pathLabel")
.append("svg:textPath")
.attr("startOffset", "50%")
.attr("text-anchor", "middle")
.attr("xlink:href", function(d) { return "#invis_" + d.source.id + "-" + d.value + "-" + d.target.id; })
.style("fill", "#cccccc")
.style("font-size", 11)
.text(function(d) { return d.value; });
var node = svg.selectAll("g.node")
.data(force.nodes());
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(force.drag);
nodeEnter.append("svg:circle")
.attr("r", 4)
.attr("id", function (d) {
return "Node;" + d.id;
})
.attr("class", "nodeStrokeClass")
.attr("fill", "#fa5c4f")
nodeEnter.append("svg:text")
.attr("class", "textClass")
.attr("x", 20)
.attr("y", ".31em")
.text(function (d) {
return d.label;
});
node.exit().remove();
function arcPath(leftHand, d) {
var x1 = leftHand ? d.source.x : d.target.x,
y1 = leftHand ? d.source.y : d.target.y,
x2 = leftHand ? d.target.x : d.source.x,
y2 = leftHand ? d.target.y : d.source.y,
dx = x2 - x1,
dy = y2 - y1,
dr = Math.sqrt(dx * dx + dy * dy),
drx = dr,
dry = dr,
sweep = leftHand ? 0 : 1;
siblingCount = countSiblingLinks(d.source, d.target)
xRotation = 0,
largeArc = 0;
if (dr === 0) {
sweep = 0;
xRotation = -180;
largeArc = 1;
drx = 120;
dry = 120;
x2 = x2 + 1;
y2 = y2 + 1;
}
if (siblingCount > 1) {
var siblings = getSiblingLinks(d.source, d.target);
var arcScale = d3.scale.ordinal()
.domain(siblings)
.rangePoints([1, siblingCount]);
drx = drx/(1 + (1/siblingCount) * (arcScale(d.value) - 1));
dry = dry/(1 + (1/siblingCount) * (arcScale(d.value) - 1));
}
return "M" + x1 + "," + y1 + "A" + drx + ", " + dry + " " + xRotation + ", " + largeArc + ", " + sweep + " " + x2 + "," + y2;
}
force.on("tick", function(e) {
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length,
k = .1 * e.alpha;
while (++i < n) q.visit(collide(nodes[i]));
node.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
path.attr("d", function(d) {
return arcPath(true, d);
});
pathInvis.attr("d", function(d) {
return arcPath(d.source.x < d.target.x, d);
});
});
force
.charge(-10)
.friction(.1)
.linkDistance(linkDistance)
.size([w, h])
.start();
keepNodesOnTop();
}
update();
function collide(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * .5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
}
function drawGraph() {
graph = new myGraph();
graph.initialize();
}
drawGraph();
function keepNodesOnTop() {
$(".nodeStrokeClass").each(function( index ) {
var gNode = this.parentNode;
gNode.parentNode.appendChild(gNode);
});
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment