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.