Skip to content

Instantly share code, notes, and snippets.

@emeeks
Last active July 28, 2016 00:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save emeeks/7914714 to your computer and use it in GitHub Desktop.
Save emeeks/7914714 to your computer and use it in GitHub Desktop.
Taffy Edges
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<style>
body { font-family: Hevetica; }
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: 0;
}
</style>
<title>Taffy Edges</title>
</head>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500;
stateChange = true;
refreshGraph = 100;
nodeSettings = {
emperor: {number: 1, color: "red", size: 15},
governor: {number: 0, color: "orange", size: 10},
hierarchical3: {number: 0, color: "yellow", size: 8},
hierarchical4: {number: 0, color: "green", size: 6},
hierarchical5: {number: 0, color: "blue", size: 4},
hierarchical6: {number: 0, color: "darkblue", size: 2},
outsiderA: {number: 0, color: "lightgray", size: 8},
outsiderB: {number: 2, color: "gray", size: 4}
}
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(function(d) {return d.weight * -30})
.linkDistance(40)
.linkStrength(function(d) {return d.weight})
.gravity(.05)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
taffyEdge = d3.svg.line().x(function(d) {return d.x}).y(function(d) {return d.y}).interpolate("basis");
function edgePoints(sourceNode, targetNode) {
var sourceWidth = Math.max(nodeSettings[sourceNode.type].size * 2) / 2
var targetWidth = Math.max(nodeSettings[targetNode.type].size * 2) / 2
var xOffset = sourceNode.x - targetNode.x;
var yOffset = sourceNode.y - targetNode.y;
if (Math.abs(xOffset) > Math.abs(yOffset)) {
sourceWidthX = 0;
sourceWidthY = sourceWidth;
targetWidthX = 0;
targetWidthY = targetWidth;
}
else {
sourceWidthX = sourceWidth;
sourceWidthY = 0;
targetWidthX = targetWidth;
targetWidthY = 0;
}
// var width = 10;
var taffyPoints = [
{x: sourceNode.x, y: sourceNode.y},
{x: sourceNode.x - sourceWidthX, y: sourceNode.y - sourceWidthY},
{x: (sourceNode.x + targetNode.x) / 2, y: (sourceNode.y + targetNode.y) / 2},
{x: targetNode.x - sourceWidthX, y: targetNode.y - sourceWidthY},
{x: targetNode.x, y: targetNode.y},
{x: targetNode.x + targetWidthX, y: targetNode.y + targetWidthY},
{x: (sourceNode.x + targetNode.x) / 2, y: (sourceNode.y + targetNode.y) / 2},
{x: sourceNode.x + targetWidthX, y: sourceNode.y + targetWidthY},
{x: sourceNode.x, y: sourceNode.y}
]
return taffyPoints;
}
numCommunities = 6;
function maxIndex(x) {
var maxValue = d3.max(x);
return x.indexOf(maxValue);
}
var graphVariable;
graph = graphConstructor();
graphVariable = graph;
graphVariable.nodes.forEach( function (d) {
d.nodeStrength = 1;
d.communities = [];
d.communityBuffer = [];
})
testCommunities();
force
.nodes(graph.nodes)
.links(graph.links)
.start();
/*
graph.nodes[0].fixed = true;
graph.nodes[0].x = 100;
graph.nodes[0].px = 100;
graph.nodes[0].y = 100;
graph.nodes[0].py = 100;
graph.nodes[62].fixed = true;
graph.nodes[62].x = 860;
graph.nodes[62].px = 860;
graph.nodes[62].y = 400;
graph.nodes[62].py = 400;
*/
force
.linkStrength(function(d) {return (d.source.nodeStrength == 0 || d.target.nodeStrength == 0) ? 0 : 1})
.charge(function(d) {return d.nodeStrength * -60})
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("path")
.attr("class", "link")
.attr("d", function (d) {return taffyEdge(edgePoints(d.source, d.target)) + "Z"})
.style("opacity", 0);
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.on("click", grayOut)
.call(force.drag);
node.append("circle")
.attr("r", 1)
.style("fill", "gray")
node.append("text")
.text(function(d) { return d.label; })
.style("stroke", "none")
.style("font-size", "0px")
.style("font-weight", 0)
.attr("text-anchor", "middle")
.style("pointer-events", "none");
d3.selectAll("g.node").select("circle")
.each(function(d,i) {
d3.select(this)
.transition()
.delay(2000 + (i * 300))
// .duration(function(d, i) {return i * 100})
.attr("r", Math.max(10, nodeSettings[d.type].size * 2))
.style("fill", nodeSettings[d.type].color)
.transition()
.duration(1000)
.attr("r", nodeSettings[d.type].size)
});
d3.selectAll("g.node").select("text")
.each(function(d,i) {
d3.select(this)
.transition()
.delay(2000 + (i * 300))
.style("font-size", function (d) {return Math.max(12, nodeSettings[d.type].size * 2) + "px"})
.style("font-weight", 500)
.transition()
.duration(1000)
.style("font-size", function (d) {return nodeSettings[d.type].size + "px"})
.style("font-weight", 150)
})
d3.selectAll("path.link")
.transition()
.delay(function(d,i) {return 2000 + (i * 300)})
.style("opacity", 1)
.transition()
.duration(1000)
.style("fill", function(d) {return d.type == "horizontal" ? "lightblue" : "lightgreen"})
force.on("tick", tick);
function tick() {
/*
if(refreshGraph > 0) {
console.log("skipped")
refreshGraph--;
return;
}
*/
force.alpha(.05);
if (Math.random() < .025) {
var randomPerturb = (Math.floor(Math.random() * (graphVariable.nodes.length)));
graphVariable.nodes[randomPerturb].nodeStrength = Math.max(0, graphVariable.nodes[randomPerturb].nodeStrength - (Math.random() > .50 ? -1 : 1))
stateChange = true;
}
if (stateChange == true) {
force.stop();
stateChange = false;
d3.selectAll("circle.node")
// .style("fill", function(d) {return d.nodeStrength == 0 ? "gray" : color(maxIndex(d.communities))})
.style("display", function(d) {return d.nodeStrength == 0 ? "none" : "block"})
.attr("opacity", function(d) {return Math.max(.33, (d.nodeStrength * .1))})
d3.selectAll("path.link")
.style("display", function(d) {return (d.source.nodeStrength == 0 || d.target.nodeStrength == 0) ? "none" : "block"});
setTimeout(function() {force.start()}, 100);
refreshGraph = 100;
}
d3.selectAll(".link")
.attr("d", function (d) {return taffyEdge(edgePoints(d.source, d.target)) + "Z"})
/* link.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; });
*/
d3.selectAll("g.node")
.attr("transform", function(d) { return "translate("+d.x+","+d.y+")"})
}
function grayOut(d,i) {
d.fixed = true;
testCommunities()
// stateChange = true;
}
function testCommunities() {
graphVariable.nodes.forEach( function(node) {
node.communities = [];
node.communityBuffer = [];
for (var community = 0; community < numCommunities; community++) {
// Initialize with a small Exponential variate
node.communities[community] = 0.01 * -Math.log(Math.random());
node.communityBuffer[community] = 0.0;
}
});
var communitySums = [];
for (var iteration = 0; iteration < 20; iteration++) {
for (var community = 0; community < numCommunities; community++) {
communitySums[community] = 0.0;
}
// Estimate community memberships for each edge
graphVariable.links.forEach( function(edge) {
// if (edge.source.nodeStrength > 0 && edge.target.nodeStrength > 0) {
var sourceCommunities = edge.source.communities;
var targetCommunities = edge.target.communities;
var distribution = [];
// Multiply the two community membership vectors
for (var community = 0; community < numCommunities; community++) {
distribution[community] = sourceCommunities[community] * targetCommunities[community];
}
// Normalize and add to the gradient
var normalizer = edge.weight / d3.sum(distribution);
for (var community = 0; community < numCommunities; community++) {
distribution[community] *= normalizer;
communitySums[community] += distribution[community];
edge.source.communityBuffer[community] += distribution[community];
edge.target.communityBuffer[community] += distribution[community];
}
// }
});
// We need to divide each node value by the square root of the community sum.
var communityNormalizers = []
for (var community = 0; community < numCommunities; community++) {
communityNormalizers[community] = 1.0 / Math.sqrt(communitySums[community]);
}
// Update parameters and clear the buffer.
graphVariable.nodes.forEach( function(node) {
for (var community = 0; community < numCommunities; community++) {
node.communities[community] = node.communityBuffer[community] * communityNormalizers[community];
node.communityBuffer[community] = 0.0;
}
});
}
d3.selectAll("circle.node")
.style("fill", function(d) { return color(maxIndex(d.communities)); })
}
function graphConstructor() {
var nodes = [], links = [];
var verticalEdgeSettings =
[
{source: "governor", target: "emperor", number: 6, numbermin: 4},
{source: "hierarchical3", target: "governor", number: 3, numbermin: 2},
{source: "hierarchical4", target: "hierarchical3", number: 3, numbermin: 1},
{source: "hierarchical5", target: "hierarchical4", number: 3, numbermin: 1},
{source: "hierarchical6", target: "hierarchical5", number: 5, numbermin: 0},
{source: "outsiderA", target: "emperor", number: 2, numbermin: 2}
]
var horizontalEdgeSettings =
[
{source: "governor", target: "governor", probability: .01},
{source: "hierarchical3", target: "hierarchical3", probability: .05},
{source: "hierarchical4", target: "hierarchical4", probability: .025},
{source: "hierarchical5", target: "hierarchical5", probability: .01},
{source: "outsiderA", target: "emperor", probability: .1},
{source: "outsiderA", target: "governor", probability: .25},
{source: "outsiderB", target: "governor", probability: .1},
{source: "outsiderB", target: "hierarchical3", probability: .2}
]
for (nodeClass in nodeSettings) {
var x = 1;
while (x <= nodeSettings[nodeClass].number) {
var topNode = {label: nodeClass + "-" + x, type: nodeClass}
nodes.push(topNode);
x++;
}
}
var node = 0;
while (node < nodes.length && node < 500) {
for (verticalEdge in verticalEdgeSettings) {
if (nodes[node].type == verticalEdgeSettings[verticalEdge].target) {
var x = 1;
var x = Math.min(verticalEdgeSettings[verticalEdge].number - verticalEdgeSettings[verticalEdge].numbermin, Math.ceil(verticalEdgeSettings[verticalEdge].number * Math.random()))
while (x < verticalEdgeSettings[verticalEdge].number) {
var spawnNode = {label: verticalEdgeSettings[verticalEdge].source + "-" + x, type: verticalEdgeSettings[verticalEdge].source}
nodes.push(spawnNode);
var newLink = {source: nodes[node], target: spawnNode, weight: 2, type: "vertical"};
links.push(newLink);
x++;
}
}
}
node++;
}
for (nodex in nodes) {
for (nodey in nodes) {
for (horizontalEdge in horizontalEdgeSettings) {
if (horizontalEdgeSettings[horizontalEdge].source == nodes[nodex].type && horizontalEdgeSettings[horizontalEdge].target == nodes[nodey].type) {
var randomChance = Math.random();
if (randomChance < horizontalEdgeSettings[horizontalEdge].probability) {
var newLink = {source: nodes[nodex], target: nodes[nodey], weight: 1, type: "horizontal"};
links.push(newLink);
}
}
}
}
}
genNodes = nodes;
genEdges = links;
var returnObject = {links: genEdges, nodes: genNodes};
return returnObject;
}
function populateLists() {
var textNodes = "<h3>Node List</h3><p>id<br>";
for (x in force.nodes()) {
textNodes += force.nodes()[x].label;
textNodes += "<br>";
}
textNodes += "</p>"
textNodes += "<h3>Edge List</h3><p>source,target,type<br>";
for (x in force.links()) {
textNodes += force.links()[x].source.label;
textNodes += ",";
textNodes += force.links()[x].target.label;
textNodes += ",";
textNodes += force.links()[x].type;
textNodes += "<br>";
}
textNodes += "</p>"
d3.select("#code").html(textNodes);
}
</script>
<p>Settings: <input type="button" value="Edge List" onclick="populateLists()" /></p>
<pre id="code">
nodeSettings = {
emperor: {number: 1, color: "red", size: 15},
hierarchical2: {number: 0, color: "orange", size: 10},
hierarchical3: {number: 0, color: "yellow", size: 8},
hierarchical4: {number: 0, color: "green", size: 6},
hierarchical5: {number: 0, color: "blue", size: 4},
hierarchical6: {number: 0, color: "darkblue", size: 2},
outsiderA: {number: 0, color: "lightgray", size: 8},
outsiderB: {number: 2, color: "gray", size: 4}
}
var verticalEdgeSettings =
[
{source: "hierarchical2", target: "emperor", number: 6, numbermin: 4},
{source: "hierarchical3", target: "hierarchical2", number: 3, numbermin: 2},
{source: "hierarchical4", target: "hierarchical3", number: 3, numbermin: 1},
{source: "hierarchical5", target: "hierarchical4", number: 3, numbermin: 1},
{source: "hierarchical6", target: "hierarchical5", number: 5, numbermin: 0},
{source: "outsiderA", target: "emperor", number: 2, numbermin: 2}
]
horizontalEdgeSettings =
[
{source: "hierarchical2", target: "hierarchical2", probability: .01},
{source: "hierarchical3", target: "hierarchical3", probability: .05},
{source: "hierarchical4", target: "hierarchical4", probability: .025},
{source: "hierarchical5", target: "hierarchical5", probability: .01},
{source: "outsiderA", target: "hierarchical1", probability: .1},
{source: "outsiderA", target: "hierarchical2", probability: .25},
{source: "outsiderB", target: "hierarchical2", probability: .1},
{source: "outsiderB", target: "hierarchical3", probability: .2}
]
</pre>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment