|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>Erdős–Rényi Force Directed Graph</title> |
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> |
|
<style type="text/css"> |
|
|
|
body { |
|
padding: 0 |
|
margin: 0 |
|
} |
|
|
|
#texts text { |
|
fill: #000; |
|
font-weight: bold; |
|
font-family: monospace; |
|
-webkit-touch-callout: none; |
|
-webkit-user-select: none; |
|
-khtml-user-select: none; |
|
-moz-user-select: -moz-none; |
|
-ms-user-select: none; |
|
user-select: none; |
|
} |
|
|
|
#lines line { |
|
stroke: #999; |
|
} |
|
|
|
</style> |
|
</head> |
|
<body> |
|
<div id="chart"></div> |
|
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/d3/2.10.0/d3.v2.min.js"></script> |
|
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js"></script> |
|
<script type="text/javascript"> |
|
var config = { "nodes" : 100, "probability" : 1, "linkDistance" : 30, "linkStrength": 0.001, "charge" : 35, "gravity" : .1 }; |
|
var gui = new dat.GUI(); |
|
var nodesChanger = gui.add(config, "nodes", 1, 200).listen().step(1); |
|
nodesChanger.onChange(function(value) { |
|
if(value != 200) { |
|
erdosReni() |
|
restart() |
|
} |
|
}); |
|
|
|
var probabilityChanger = gui.add(config, "probability", 0, 100); |
|
probabilityChanger.onChange(function(value) { |
|
erdosReni() |
|
restart() |
|
}); |
|
|
|
var fl = gui.addFolder('Force Layout'); |
|
|
|
var linkDistanceChanger = fl.add(config, "linkDistance", 0, 400); |
|
linkDistanceChanger.onChange(function(value) { |
|
force.linkDistance(value) |
|
restart() |
|
}); |
|
|
|
var linkStrengthChanger = fl.add(config, "linkStrength", 0, 1); |
|
linkStrengthChanger.onChange(function(value) { |
|
force.linkStrength(value) |
|
restart() |
|
}); |
|
|
|
var chargeChanger = fl.add(config,"charge", 0, 500); |
|
chargeChanger.onChange(function(value) { |
|
force.charge(-value) |
|
restart() |
|
}); |
|
|
|
var gravityChanger = fl.add(config,"gravity", 0, 1); |
|
gravityChanger.onChange(function(value) { |
|
force.gravity(value) |
|
restart() |
|
}); |
|
|
|
config.regenerate = function() { |
|
erdosReni() |
|
restart() |
|
} |
|
gui.add(config, 'regenerate') |
|
|
|
var width = window.innerWidth-245, |
|
height = window.innerHeight, |
|
radius = 10, |
|
maxLinks = 5000, |
|
drawMax = null, |
|
nodes = [], |
|
links = []; |
|
|
|
var zoom = d3.behavior.zoom() |
|
.scale(config["nodes"]) |
|
.scaleExtent([1, 200]) |
|
.on("zoom", function(d,i) { |
|
config["nodes"] = Math.ceil(d3.event.scale) |
|
erdosReni() |
|
restart() |
|
}); |
|
|
|
var svg = d3.select("#chart").append("svg") |
|
.attr("width", width) |
|
.attr("height", height) |
|
.call(zoom) |
|
|
|
lines = svg.append("g").attr("id", "lines") |
|
texts = svg.append("g").attr("id", "texts") |
|
|
|
var force = d3.layout.force() |
|
.linkDistance(config["linkDistance"]) |
|
.linkStrength(config["linkStrength"]) |
|
.gravity(config["gravity"]) |
|
.size([width, height]) |
|
.charge(-config["charge"]); |
|
|
|
erdosReni(); |
|
restart(); |
|
|
|
function erdosReni() { |
|
numNodes = config["nodes"] |
|
if(nodes.length < numNodes) { |
|
for(i=nodes.length; numNodes>nodes.length; i++){ |
|
// http://en.wikipedia.org/wiki/Archimedean_spiral |
|
angle = 2 * i; |
|
nodes.push({x: angle*Math.cos(angle)+(width/2), y: angle*Math.sin(angle)+(height/2)}); |
|
} |
|
} else if(nodes.length > numNodes) { |
|
nodes.length = numNodes |
|
} |
|
|
|
links = [] |
|
linksIndex = {} |
|
nodes.forEach(function(node, nodei) { |
|
nodes.forEach(function(node2, node2i) { |
|
//check the probabilty of an edge once and don't link to self |
|
if (linksIndex[nodei + "," + node2i] || linksIndex[node2i + "," + nodei] || nodei == node2i) return |
|
linksIndex[nodei + "," + node2i] = 1; |
|
|
|
if (Math.random() < config["probability"] * .01) { |
|
links.push({source: node, target: node2}); |
|
} |
|
}) |
|
}) |
|
|
|
if(links.length > maxLinks) { |
|
if(drawMax == true || drawMax == null && confirm("draw more than "+links.length+" edges?")) { |
|
drawMax = true |
|
} else { |
|
links.length = maxLinks |
|
drawMax = false |
|
} |
|
} |
|
force.nodes(nodes).links(links) |
|
} |
|
|
|
function restart() { |
|
force.start(); |
|
|
|
link = lines.selectAll("#lines line") |
|
.data(links) |
|
|
|
link.enter().insert("line") |
|
.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; }); |
|
|
|
link.exit().remove() |
|
|
|
node = texts.selectAll("#texts text") |
|
.data(nodes) |
|
|
|
node.enter().insert("text") |
|
.call(force.drag); |
|
|
|
node.text(function(d) { return d.weight; }) |
|
.style("fill", function(d) { return d.weight == 0 ? "darkred" : "black" }) |
|
|
|
node.exit().remove() |
|
} |
|
|
|
force.on("tick", function() { |
|
svg.selectAll("#lines line") |
|
.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; }) |
|
|
|
svg.selectAll("#texts text") |
|
.attr("transform", function(d) { |
|
return "translate("+d.x+"," + d.y+ ")" |
|
}) |
|
}); |
|
|
|
</script> |
|
</body> |
|
</html> |