Created by Christopher Manning
Draws a force directed graph using svg:path and geo projections onto a sphere.
- Make links in rectangular projection wraparound
- Fix node replusion in azimuthal projections
- Fix node dragging in azimuthal projections
Created by Christopher Manning
Draws a force directed graph using svg:path and geo projections onto a sphere.
<!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> |
@christophermanning : Here is a sample example https://jsfiddle.net/kfpa14gm/5/ I have created with adding labels on node with this graph but labels are coming as a spherical view around the node I want it on the node like simple force layout. I tried many ways but not yet find a way to solve this. your urgent help will be appreciated.
hello @christophermanning, This is an excellent example of spherical force directed using d3. I am using this example and currently I am facing issue for adding label on node by trying various d3 methods for append text on node but couldn't work. can you please help me out in this example how can I add labels on nodes?