|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>Spherical Force-Directed Layout</title> |
|
<script src="http://d3js.org/d3.v3.min.js"></script> |
|
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js"></script> |
|
<!--<script src="/js/d3.v3.min.js"></script>--> |
|
<!--<script src="/js/dat-gui/build/dat.gui.js"></script> --> |
|
<style type="text/css"> |
|
body { |
|
padding: 0; |
|
margin: 0; |
|
} |
|
path.node { |
|
stroke-width: 1.5px; |
|
} |
|
|
|
path.link { |
|
stroke: #999; |
|
fill-opacity: 0 |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<script type="text/javascript"> |
|
var projections = { |
|
"Albers": d3.geo.albers(), |
|
"Azimuthal Equal Area": d3.geo.azimuthalEqualArea(), |
|
"Azimuthal Eqidistant": d3.geo.azimuthalEquidistant(), |
|
"Conic Conformal": d3.geo.conicConformal(), |
|
"Conic Equal Area": d3.geo.conicEqualArea(), |
|
"Conic Equidistant": d3.geo.conicEquidistant(), |
|
"Eqirectangular": d3.geo.equirectangular(), |
|
"Gnomonic": d3.geo.gnomonic(), |
|
"Mercator": d3.geo.mercator(), |
|
"Orthographic": d3.geo.orthographic(), |
|
"Stereographic": d3.geo.stereographic(), |
|
"Transverse Mercator": d3.geo.transverseMercator(), |
|
}; |
|
var config = { "projection": "Orthographic", "clip": true, "friction": .9, "linkStrength": 1, "linkDistance": 20, "charge": 30, "gravity": .1, "theta": .8 }; |
|
var gui = new dat.GUI(); |
|
//var projectionChanger = gui.add(config, "projection", ['equalarea', 'equidistant', 'gnomonic', 'orthographic', 'stereographic', 'rectangular']); |
|
var projectionChanger = gui.add(config, "projection", Object.keys(projections)); |
|
//http://stackoverflow.com/a/3417242 |
|
function wrapIndex(i, i_max) { |
|
return ((i % i_max) + i_max) % i_max; |
|
} |
|
projectionChanger.onChange(function(value) { |
|
projection = projections[value] |
|
.scale(height/2) |
|
.translate([(width/2)-125, height/2]) |
|
.clipAngle(config["clip"] ? 90 : null) |
|
|
|
path.projection(projections[value]) |
|
return |
|
if(value == 'rectangular') { |
|
path = d3.geo.path().projection(function(coordinates){ |
|
console.log(coordinates[0], coordinates[1]) |
|
return [ |
|
wrapIndex(coordinates[0], width), |
|
wrapIndex(coordinates[1], height), |
|
]; |
|
}); |
|
config['clip'] = false |
|
} else { |
|
projection.mode(value) |
|
path = d3.geo.path().projection(projection) |
|
} |
|
|
|
force.start() |
|
}); |
|
|
|
var clipChanger = gui.add(config, "clip").listen(); |
|
clipChanger.onChange(function(value) { |
|
projection.clipAngle(value ? 90 : null) |
|
force.start() |
|
}); |
|
|
|
var fl = gui.addFolder('Force Layout'); |
|
fl.open() |
|
|
|
var frictionChanger = fl.add(config, "friction", 0, 1); |
|
frictionChanger.onChange(function(value) { |
|
force.friction(value) |
|
force.start() |
|
}); |
|
|
|
var linkDistanceChanger = fl.add(config, "linkDistance", 0, 400); |
|
linkDistanceChanger.onChange(function(value) { |
|
force.linkDistance(value) |
|
force.start() |
|
}); |
|
|
|
var linkStrengthChanger = fl.add(config, "linkStrength", 0, 1); |
|
linkStrengthChanger.onChange(function(value) { |
|
force.linkStrength(value) |
|
force.start() |
|
}); |
|
|
|
var chargeChanger = fl.add(config,"charge", 0, 500); |
|
chargeChanger.onChange(function(value) { |
|
force.charge(-value) |
|
force.start() |
|
}); |
|
|
|
var gravityChanger = fl.add(config,"gravity", 0, 1); |
|
gravityChanger.onChange(function(value) { |
|
force.gravity(value) |
|
force.start() |
|
}); |
|
|
|
var thetaChanger = fl.add(config,"theta", 0, 1); |
|
thetaChanger.onChange(function(value) { |
|
force.theta(value) |
|
force.start() |
|
}); |
|
|
|
var width = window.innerWidth, |
|
height = window.innerHeight - 5, |
|
fill = d3.scale.category20(), |
|
nodes = [{x: width/2, y: height/2}], |
|
links = []; |
|
|
|
var projection = projections[config["projection"]] |
|
.scale(height/2) |
|
.translate([(width/2)-125, height/2]) |
|
.clipAngle(config["clip"] ? 90 : null) |
|
|
|
var path = d3.geo.path() |
|
.projection(projection) |
|
|
|
var force = d3.layout.force() |
|
.linkDistance(config["linkDistance"]) |
|
.linkStrength(config["linkStrength"]) |
|
.gravity(config["gravity"]) |
|
.size([width, height]) |
|
.charge(-config["charge"]); |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width) |
|
.attr("height", height) |
|
.call(d3.behavior.drag() |
|
.origin(function() { var r = projection.rotate(); return {x: 2 * r[0], y: -2 * r[1]}; }) |
|
.on("drag", function() { force.start(); var r = [d3.event.x / 2, -d3.event.y / 2, projection.rotate()[2]]; t0 = Date.now(); origin = r; projection.rotate(r); })) |
|
|
|
for(x=0;x<100;x++){ |
|
source = nodes[~~(Math.random() * nodes.length)] |
|
target = {x: source.x + Math.random(), y: source.y + Math.random(), group: Math.random()} |
|
links.push({source: source, target: target}) |
|
nodes.push(target) |
|
} |
|
|
|
var link = svg.selectAll("path.link") |
|
.data(links) |
|
.enter().append("path").attr("class", "link") |
|
|
|
var node = svg.selectAll("path.node") |
|
.data(nodes) |
|
.enter().append("path").attr("class", "node") |
|
.style("fill", function(d) { return fill(d.group); }) |
|
.style("stroke", function(d) { return d3.rgb(fill(d.group)).darker(); }) |
|
.call(force.drag); |
|
|
|
force |
|
.nodes(nodes) |
|
.links(links) |
|
.on("tick", tick) |
|
.start(); |
|
|
|
function tick() { |
|
node.attr("d", function(d) { var p = path({"type":"Feature","geometry":{"type":"Point","coordinates":[d.x, d.y]}}); return p ? p : 'M 0 0' }); |
|
link.attr("d", function(d) { var p = path({"type":"Feature","geometry":{"type":"LineString","coordinates":[[d.source.x, d.source.y],[d.target.x, d.target.y]]}}); return p ? p : 'M 0 0' }); |
|
} |
|
</script> |
|
</body> |
|
</html> |