Skip to content

Instantly share code, notes, and snippets.

@allisonking
Last active September 10, 2017 03:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save allisonking/49e10b78dc09c56a43bb7b56b983b34d to your computer and use it in GitHub Desktop.
Save allisonking/49e10b78dc09c56a43bb7b56b983b34d to your computer and use it in GitHub Desktop.
Saving off points from a force directed graph

This is part one of a way to use d3.js's force directed graph to create a customized static layout. Part 2 is here. The simulation first distributes all of the nodes and links according to forces and charges. Then, you can drag the nodes around to where you would want them to be in a final static image. You can also drag the purple 'intermediate' nodes around in order to specify the control points for the Bezier curve of the links.

This was largely inspired by Mike Bostock's post on his process for making the NYT Oscar Contenders graph which also uses an adjusted force directed graph layout with Bezier control points.

Once you are happy with the way your graph looks, you can click the 'print' button and a JSON object will be printed to your console. This is a quick and very dirty way to get the object- in Google Chrome you can right click on the variable through developer tools and make it global. This should yield a variable called temp1. Then type copy(temp1) into your console and the JSON will be copied to your clipboard. Copy that into a separate file, and read it in- see part 2 for an example of reading in a file like that one to generate a static graph. This isn't a great way to do it, but it certainly gets the job done. Feel free to fork to add better functionality!

function Graph(options) {
d3.select('#printbutton').on('click', printInfo);
function printInfo(){
var output = {};
var nodeInfo = [];
var interInfo = [];
var linkInfo = [];
d3.selectAll('.node')
.filter(function(d){
d.fx = d.x;
d.fy = d.y;
d.vx = 0;
d.vy = 0;
nodeInfo.push(d);
});
d3.selectAll('.intermediate')
.filter(function(d){
d.fx = d.x;
d.fy = d.y;
d.vx = 0;
d.vy = 0;
interInfo.push(d);
});
output['nodes'] = nodeInfo;
output['intermediates'] = interInfo;
output['links'] = data_links;
console.log(output);
}
var svg = d3.select(options.container),
width = +svg.attr("width"),
height = +svg.attr("height");
var colorMap = {
'Gryffindor' : 'red',
'Slytherin' : 'green',
'Hufflepuff' : 'yellow',
'Ravenclaw' : 'blue',
'other' : 'grey'
}
var linkScale = d3.scaleLog()
.range([1, 6])
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(20).strength(0.5))
.force("charge", d3.forceManyBody().strength(-80))
.force("center", d3.forceCenter(width / 2, height / 2));
// global variable to save off the links easily
var data_links;
d3.json("potter.json", function(error, graph) {
if (error) throw error;
// make a deep copy of the links
data_links = JSON.parse(JSON.stringify(graph.links));
var nodes = graph.nodes,
nodeById = d3.map(nodes, function(d) { return d.id; }),
links = graph.links,
bilinks = [];
links.forEach(function(link) {
var s = link.source = nodeById.get(link.source),
t = link.target = nodeById.get(link.target),
v = +link.value,
i = {}; // intermediate node
nodes.push(i);
links.push({source: s, target: i}, {source: i, target: t});
bilinks.push([s, i, t, v]);
});
linkScale.domain([d3.min(bilinks, function(d) { return d[3]; }),
d3.max(bilinks, function(d) { return d[3]; })]);
var link = svg.selectAll(".link")
.data(bilinks)
.enter().append("path")
.attr("class", "link")
.attr('stroke-width', function(d) {
return linkScale(d[3]);
})
.style('stroke', 'grey')
.on('mouseover', handleLinkMouseOver)
.on('mouseout', handleLinkMouseOut);
link.append('svg:title')
.text(function(d){ return d[3]; });
var node = svg.selectAll('.node')
.data(nodes.filter(function(d) { return d.id; }))
.enter().append('g')
.attr('class', 'node')
.on('mouseover', handleNodeMouseOver)
.on('mouseout', handleNodeMouseOut)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged));
// comment out ending the drag so that nodes don't return to natural position
//.on("end", dragended));
node.append('circle')
.attr('r', 20)
.style("stroke", function(d) { return colorMap[d.house]; })
.style('stroke-width', 2)
.attr('fill', 'white');
node.append('text')
.attr('class', 'node-label')
.attr('fill', 'black')
.attr('y', function(d) { return '.35em'})
.attr('text-anchor', 'middle')
.text(function(d) { return getInitials(d.id)});
// these are the invisible nodes that make the bezier curve
var intermediates = svg.selectAll('.intermediate')
.data(nodes.filter(function(d) {
return typeof d.id == 'undefined';
}))
.enter().append('circle')
.attr('class', 'intermediate')
.attr('r', 2)
.attr('fill', 'purple')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged));
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
function ticked() {
link.attr("d", positionLink);
node.attr("transform", positionNode);
intermediates.attr('transform', positionNode);
}
});
function getInitials(name) {
if(name == 'OC') return name;
var initials = "";
var split = name.split(" ");
split.forEach(function(partial_name) {
initials = initials + partial_name[0];
})
return initials;
}
function positionLink(d) {
return "M" + d[0].x + "," + d[0].y
+ "S" + d[1].x + "," + d[1].y
+ " " + d[2].x + "," + d[2].y;
}
function positionNode(d) {
return "translate(" + d.x + "," + d.y + ")";
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x, d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x, d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null, d.fy = null;
}
function handleNodeMouseOver(d) {
d3.select(this)
.select('text')
.text(d.id);
d3.selectAll('.link')
.filter(function(link) {
return link[0].id == d.id || link[2].id == d.id;
})
.style('stroke', 'rgb(50, 118, 231)');
}
function handleNodeMouseOut(d) {
d3.select(this)
.select('text')
.text(getInitials(d.id));
d3.selectAll('.link')
.style('stroke', 'grey');
}
function handleLinkMouseOver(d) {
d3.select(this)
.style('stroke', 'rgb(50, 118, 231)')
d3.selectAll('.node')
.filter(function(node) {
return node.id == d[0].id || node.id == d[2].id;
})
.selectAll('circle')
.style('stroke-width', 5);
}
function handleLinkMouseOut(d) {
d3.select(this)
.style('stroke', 'grey');
d3.selectAll('.node')
.selectAll('circle')
.style('stroke-width', 2);
}
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
fill: none;
}
</style>
<button id='printbutton'> print</button>
<svg width="960" height="600" id="graph-container"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src='graph-arrange.js'></script>
<script>
Graph({
container: "#graph-container"
});
</script>
{"nodes": [{"id": "Harry P.", "generation": "main_gen", "house": "Gryffindor"}, {"id": "Hermione G.", "generation": "main_gen", "house": "Gryffindor"}, {"id": "Draco M.", "generation": "main_gen", "house": "Slytherin"}, {"id": "Severus S.", "generation": "marauder_gen", "house": "Slytherin"}, {"id": "Lily Evans P.", "generation": "marauder_gen", "house": "Gryffindor"}, {"id": "James P.", "generation": "marauder_gen", "house": "Gryffindor"}, {"id": "Sirius B.", "generation": "marauder_gen", "house": "Gryffindor"}, {"id": "Ginny W.", "generation": "main_gen", "house": "Gryffindor"}, {"id": "Ron W.", "generation": "main_gen", "house": "Gryffindor"}, {"id": "Remus L.", "generation": "marauder_gen", "house": "Gryffindor"}, {"id": "OC", "generation": "other", "house": "other"}, {"id": "Scorpius M.", "generation": "next_gen", "house": "other"}, {"id": "Voldemort", "generation": "riddle_gen", "house": "Slytherin"}, {"id": "George W.", "generation": "main_gen", "house": "Gryffindor"}, {"id": "Luna L.", "generation": "main_gen", "house": "Gryffindor"}, {"id": "Fred W.", "generation": "main_gen", "house": "Gryffindor"}, {"id": "Albus D.", "generation": "riddle_gen", "house": "Gryffindor"}, {"id": "Rose W.", "generation": "next_gen", "house": "other"}, {"id": "N. Tonks", "generation": "main_gen", "house": "Hufflepuff"}, {"id": "Tom R. Jr.", "generation": "riddle_gen", "house": "Slytherin"}, {"id": "Albus S. P.", "generation": "next_gen", "house": "other"}, {"id": "Lily Luna P.", "generation": "next_gen", "house": "other"}, {"id": "Bellatrix L.", "generation": "marauder_gen", "house": "Slytherin"}, {"id": "Angelina J.", "generation": "main_gen", "house": "Gryffindor"}, {"id": "Neville L.", "generation": "main_gen", "house": "Gryffindor"}, {"id": "Minerva M.", "generation": "riddle_gen", "house": "Gryffindor"}, {"id": "Gellert G.", "generation": "riddle_gen", "house": "other"}, {"id": "James S. P.", "generation": "next_gen", "house": "other"}, {"id": "Charlie W.", "generation": "main_gen", "house": "Gryffindor"}], "links": [{"source": "Harry P.", "target": "Draco M.", "value": 34761}, {"source": "Harry P.", "target": "Hermione G.", "value": 27888}, {"source": "Harry P.", "target": "Ginny W.", "value": 22153}, {"source": "Harry P.", "target": "Severus S.", "value": 12273}, {"source": "Harry P.", "target": "Ron W.", "value": 9173}, {"source": "Hermione G.", "target": "Draco M.", "value": 44837}, {"source": "Hermione G.", "target": "Ron W.", "value": 26687}, {"source": "Hermione G.", "target": "Severus S.", "value": 11213}, {"source": "Hermione G.", "target": "Ginny W.", "value": 3583}, {"source": "Draco M.", "target": "Ginny W.", "value": 12946}, {"source": "Draco M.", "target": "OC", "value": 4130}, {"source": "Draco M.", "target": "Ron W.", "value": 2612}, {"source": "Severus S.", "target": "Lily Evans P.", "value": 6619}, {"source": "Severus S.", "target": "OC", "value": 2816}, {"source": "Severus S.", "target": "Remus L.", "value": 2149}, {"source": "Lily Evans P.", "target": "James P.", "value": 33888}, {"source": "Lily Evans P.", "target": "Sirius B.", "value": 3648}, {"source": "Lily Evans P.", "target": "Remus L.", "value": 2285}, {"source": "Lily Evans P.", "target": "Harry P.", "value": 2221}, {"source": "James P.", "target": "Sirius B.", "value": 8342}, {"source": "James P.", "target": "Remus L.", "value": 3620}, {"source": "James P.", "target": "Harry P.", "value": 2593}, {"source": "James P.", "target": "Severus S.", "value": 1247}, {"source": "Sirius B.", "target": "Remus L.", "value": 17923}, {"source": "Sirius B.", "target": "Harry P.", "value": 4805}, {"source": "Sirius B.", "target": "OC", "value": 4161}, {"source": "Ginny W.", "target": "Ron W.", "value": 1646}, {"source": "Ginny W.", "target": "Tom R. Jr.", "value": 947}, {"source": "Ron W.", "target": "OC", "value": 747}, {"source": "Remus L.", "target": "N. Tonks", "value": 6008}, {"source": "Remus L.", "target": "Harry P.", "value": 2189}, {"source": "OC", "target": "Harry P.", "value": 6345}, {"source": "OC", "target": "Hermione G.", "value": 1795}, {"source": "Scorpius M.", "target": "Rose W.", "value": 7893}, {"source": "Scorpius M.", "target": "Albus S. P.", "value": 3316}, {"source": "Scorpius M.", "target": "Lily Luna P.", "value": 1808}, {"source": "Scorpius M.", "target": "Draco M.", "value": 930}, {"source": "Scorpius M.", "target": "OC", "value": 690}, {"source": "Voldemort", "target": "Harry P.", "value": 5256}, {"source": "Voldemort", "target": "Bellatrix L.", "value": 1255}, {"source": "Voldemort", "target": "Tom R. Jr.", "value": 796}, {"source": "Voldemort", "target": "Severus S.", "value": 756}, {"source": "Voldemort", "target": "Hermione G.", "value": 615}, {"source": "George W.", "target": "Fred W.", "value": 4824}, {"source": "George W.", "target": "Hermione G.", "value": 1699}, {"source": "George W.", "target": "OC", "value": 1239}, {"source": "George W.", "target": "Angelina J.", "value": 910}, {"source": "George W.", "target": "Harry P.", "value": 810}, {"source": "Luna L.", "target": "Harry P.", "value": 2821}, {"source": "Luna L.", "target": "Neville L.", "value": 1929}, {"source": "Luna L.", "target": "Draco M.", "value": 1800}, {"source": "Luna L.", "target": "Hermione G.", "value": 1004}, {"source": "Luna L.", "target": "Ginny W.", "value": 942}, {"source": "Fred W.", "target": "Hermione G.", "value": 3017}, {"source": "Fred W.", "target": "OC", "value": 1568}, {"source": "Fred W.", "target": "Harry P.", "value": 646}, {"source": "Fred W.", "target": "Angelina J.", "value": 549}, {"source": "Albus D.", "target": "Minerva M.", "value": 2941}, {"source": "Albus D.", "target": "Harry P.", "value": 2239}, {"source": "Albus D.", "target": "Severus S.", "value": 1948}, {"source": "Albus D.", "target": "Gellert G.", "value": 662}, {"source": "Albus D.", "target": "Voldemort", "value": 445}, {"source": "Rose W.", "target": "Albus S. P.", "value": 1580}, {"source": "Rose W.", "target": "James S. P.", "value": 584}, {"source": "Rose W.", "target": "Hermione G.", "value": 508}, {"source": "Rose W.", "target": "OC", "value": 504}, {"source": "N. Tonks", "target": "Harry P.", "value": 572}, {"source": "N. Tonks", "target": "Sirius B.", "value": 350}, {"source": "N. Tonks", "target": "Charlie W.", "value": 328}, {"source": "N. Tonks", "target": "Severus S.", "value": 260}, {"source": "Tom R. Jr.", "target": "Harry P.", "value": 2278}, {"source": "Tom R. Jr.", "target": "Hermione G.", "value": 1140}, {"source": "Tom R. Jr.", "target": "OC", "value": 926}]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment