Space out nodes evenly around root node in D3 force layout - javascript

I am just starting on D3, so if anyone has any general suggestions on thing I might not be doing correctly/optimally, please let me know :)
I am trying to create a Force Directed graph with the nodes spaced out evenly (or close enough) around the center root node (noted by the larger size).
Here's an example of the layout I'm trying to achieve (I understand it won't be the same every time):
I have the following graph:
var width = $("#theVizness").width(),
height = $("#theVizness").height();
var color = d3.scale.ordinal().range(["#ff0000", "#fff000", "#ff4900"]);
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("#theVizness").append("svg")
.attr("width", width)
.attr("height", height);
var loading = svg.append("text")
.attr("class", "loading")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Loading...");
/*
ForceDirectData.json
{
"nodes":[
{"name":"File1.exe","colorGroup":0},
{"name":"File2.exe","colorGroup":0},
{"name":"File3.exe","colorGroup":0},
{"name":"File4.exe","colorGroup":0},
{"name":"File5.exe","colorGroup":0},
{"name":"File6.exe","colorGroup":0},
{"name":"File7.exe","colorGroup":0},
{"name":"File8.exe","colorGroup":0},
{"name":"File8.exe","colorGroup":0},
{"name":"File9.exe","colorGroup":0}
],
"links":[
{"source":1,"target":0,"value":10},
{"source":2,"target":0,"value":35},
{"source":3,"target":0,"value":50},
{"source":4,"target":0,"value":50},
{"source":5,"target":0,"value":65},
{"source":6,"target":0,"value":65},
{"source":7,"target":0,"value":81},
{"source":8,"target":0,"value":98},
{"source":9,"target":0,"value":100}
]
}
*/
d3.json("https://dl.dropboxusercontent.com/u/5772230/ForceDirectData.json", function (error, json) {
var nodes = json.nodes;
force.nodes(nodes)
.links(json.links)
.linkDistance(function (d) {
return d.value * 1.5;
})
.charge(function(d){
var charge = -500;
if (d.index === 0) charge = 0;
return charge;
})
.friction(0.4);
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", 1);
var files = svg.selectAll(".file")
.data(json.nodes)
.enter().append("circle")
.attr("class", "file")
.attr("r", 10)
.attr("fill", function (d) {
return color(d.colorGroup);
});
var totalNodes = files[0].length;
files.append("title")
.text(function (d) { return d.name; });
force.start();
for (var i = totalNodes * totalNodes; i > 0; --i) force.tick();
nodes[0].x = width / 2;
nodes[0].y = height / 2;
link.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; });
files.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("class", function(d){
var classString = "file"
if (d.index === 0) classString += " rootFile";
return classString;
})
.attr("r", function(d){
var radius = 10;
if (d.index === 0) radius = radius * 2;
return radius;
});
force.on("tick", function() {
link.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; });
files.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
loading.remove();
});
JSFiddle
I have already tried getting close to this with the charge() method. I thought giving every node besides the root node a higher charge would accomplish this, but it did not.
What can I do to have the child nodes evenly spaced around the root node?

Yes, force layout is a perfect tool for situations like yours.
You just need to change a little initialization of the layout, like this
force.nodes(nodes)
.links(json.links)
.charge(function(d){
var charge = -500;
if (d.index === 0) charge = 10 * charge;
return charge;
});
and voila
Explanation. I had to remove settings for friction and linkDistance since they affected placement in a bad way. The charge for root node is 10 times larger so that all other nodes are dominantly pushed away from the root. Other nodes also repel each other, and the perfect symmetry is achieved at the end, as a result.
Jsfiddle is here.
I see from your code that you attempted to affect distance from the root node and other nodes by utilizing linkDistance that is dependant on data. However, it might be better (although counter-intuitive) to use linkStrength for that purpose, like this
force.nodes(nodes)
.links(json.links)
.linkStrength(function (d) {
return d.value / 100.0;
})
.charge(function(d){
var charge = -500;
if (d.index === 0) charge = 10 * charge;
return charge;
});
but you need to experiment.
For centering and fixing the root node, you can use this
nodes[0].fixed = true;
nodes[0].x = width / 2;
nodes[0].y = height / 2;
but before initialization of layout, like in this Jsfiddle.

Related

What settings to choose for force directed graph layout in d3

I am visualising a graph of relationships between people with d3. All nodes are connected to a single central node, and then have relationships with other nodes. I've got the basics working, but I'm struggling to work out how to set the parameters like linkDistance, linkStrength, gravity and charge.
Each edge has a rating from 0-5, which I'm then using to compute linkDistance using an inverse linear scale. The main problem is getting the relationships to be represented properly. The central node seems to be much further away than any other node, even though it has the shortest linkDistance with some other nodes. I'm also finding it difficult to find the right settings to get nodes to be an appropriate distance apart.
var h = 500, w = 1000
var color = d3.scale.category20()
var svg = d3.select("body")
.append("svg")
.attr({ height: h, width: w })
queue()
.defer(d3.json, "nodes.json")
.defer(d3.json, "links.json")
.await(makeDiag);
function makeDiag(error, nodes, links, table) {
links = links.filter(function(link) {
if (link.value) return true
})
var scale = d3.scale.linear().domain([0,5]).range([20,0])
var edges = svg.selectAll("line")
.data(links)
.enter()
.append("line")
.style("stroke", "#ccc")
.style("stroke-width", 1)
/* Establish the dynamic force behavor of the nodes */
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([w,h])
.linkDistance(function(d) {
if (d.value == 0) return null
console.log('in',d.value,'out',scale(d.value))
return scale(d.value)
})
.charge(-1400)
.start();
/* Draw the edges/links between the nodes */
var texts = svg.selectAll("text")
.data(nodes)
.enter()
.append("text")
.attr("fill", "black")
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.text(function(d) { return d.name; });
var nodes = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("r", function(d,i) { return 20 })
.attr("opacity", 0.7)
.style("fill", function(d,i) { return color(i); })
.call(force.drag);
/* Draw the nodes themselves */
/* Run the Force effect */
force.on("tick", function() {
edges.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; });
nodes.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
texts.attr("transform", function(d) {
return "translate(" + (d.x - 12.5) + "," + (d.y + 5) + ")";
});
});
};
jsFiddle
full screen result

Addition of new javascript results in graph not being recognised in d3

I've added a new peice of javascript to an old script I had to add a highlighting functionality to a force network layout. I get the information for the diagram from generated json in a rails app. The original code I've been using is here:
var width = 960,
height = 960;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-100)
.linkDistance(530)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var endpoint = window.location.href+".json"
d3.json(endpoint, function(graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("marker-end", "url(#suit)");
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 10)
.style("fill", function(d) { return color(d.group); })
.call(force.drag)
.on('dblclick', connectedNodes);
node.append("title")
.text(function(d) { return d.name; });
force.on("tick", function() {
link.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; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
svg.append("defs").selectAll("marker")
.data(["suit", "licensing", "resolved"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 25)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5 L10,0 L0, -5")
.style("stroke", "#4679BD")
.style("opacity", "0.6");
//APPENDED CODE ADDED HERE
//Toggle stores whether the highlighting is on
var toggle = 0;
//Create an array logging what is connected to what
var linkedByIndex = {};
for (i = 0; i < graph.nodes.length; i++) {
linkedByIndex[i + "," + i] = 1;
};
graph.links.forEach(function (d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
//This function looks up whether a pair are neighbours
function neighboring(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
function connectedNodes() {
if (toggle == 0) {
//Reduce the opacity of all but the neighbouring nodes
d = d3.select(this).node().__data__;
node.style("opacity", function (o) {
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
});
link.style("opacity", function (o) {
return d.index==o.source.index | d.index==o.target.index ? 1 : 0.1;
});
//Reduce the op
toggle = 1;
} else {
//Put them back to opacity=1
node.style("opacity", 1);
link.style("opacity", 1);
toggle = 0;
}
}
I then tried to append further code as suggested here and simply added the following to the bottom of the script above where it is marked in capital letters
Could have been so simple.... The script worked but the added functionlity (to add highlights between nodes) didn't. An error message says:
Uncaught ReferenceError: graph is not defined
My susipicion is that it relates to the line
d3.json(endpoint, function(graph) {
and the fact that the subsequent }); is in the wrong place to encompass the new code but I've played with it and I'm not sure how to correct it
UPDATE
I've solved this. The problem was simply that I was declaring graph inside a function and the other functions couldn't access it. The solution is to put the other functions inside the function that delares it which in effect means moving the last
});
from the line
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
to the very last line. Works fine now
The answer is now given in the UPDATE section of the question

D3js force duplicate nodes on enter()

I am having some issues with d3js and I can't figure out what is going on. The idea is to draw initial graph from some endpoint data (first img), that's fine works well. Each node is clickable, on click ajax call is made for that node and data is returned, based on some criteria at that point nodes.push(xx), links.push(xx) happens to add new nodes and restart() is called to draw new nodes and links. The issue is that the main graph is doing the correct thing (Not showed on screenshots as I had to put fake data on the first graph i.e. calling an endpoint /record/id/first doesn't return a data) but there are bunch of random nodes showing up in the right bottom corner.
You can also see on the example below, even if the data doesn't change after clicking on first/second/third something wrong goes with node.enter() after restart() with the same data passed in...
JS FIDDLE: http://jsfiddle.net/5754j86e/
var w = 1200,
h = 1200;
var nodes = [];
var links = [];
var node;
var link;
var texts;
var ids = [];
var circleWidth = 10;
var initialIdentifier = "marcin";
nodes = initialBuildNodes(initialIdentifier, sparql);
links = initialBuildLinks(sparql);
//Add SVG
var svg = d3.select('#chart').append('svg')
.attr('width', w)
.attr('height', h);
var linkGroup = svg.append("svg:g").attr("id", "link-group");
var nodeGroup = svg.append("svg:g").attr("id", "node-group");
var textGroup = svg.append("svg:g").attr("id", "text-group");
//Add Force Layout
var force = d3.layout.force()
.size([w, h])
.gravity(.05)
.charge(-1040);
force.linkDistance(120);
restart();
function restart() {
force.links(links)
console.log("LINKS ARE: ", links)
link = linkGroup.selectAll(".link").data (links);
link.enter().append('line')
.attr("class", "link");
link.exit().remove();
force.nodes(nodes)
console.log("NODES ARE: ", nodes)
node = nodeGroup.selectAll(".node").data (nodes);
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
})
.on("click", function(d) {
nodeClicked (d);
})
.on('mouseenter', function(d){
nodeMouseEnter(d)
})
.on('mouseout', function(d){
nodeMouseOut(d)
});
node.exit().remove();
var annotation = textGroup.selectAll(".annotation").data (nodes);
annotation.enter().append("svg:g")
.attr("class", "annotation")
.append("text")
.attr("x", function(d) { return d.radius + 4 })
.attr("y", ".31em")
.attr("class", "label")
.text(function(d) { return d.name; });
annotation.exit().remove();
force.start();
}
function nodeClicked (d) {
// AJAX CALL happens here and bunch of nodes.push({name: "new name"}) happen
}
force.on('tick', function(e) {
link
.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 })
node.attr('transform', function(d, i) {
return 'translate('+ d.x +', '+ d.y +')';
})
svg.selectAll(".annotation").attr("transform", function(d) {
var labelx = d.x + 13;
return "translate(" + labelx + "," + d.y + ")";
})
});
Okay I got it, based on the docs (https://github.com/mbostock/d3/wiki/Selections#enter):
var update_sel = svg.selectAll("circle").data(data)
update_sel.attr(/* operate on old elements only */)
update_sel.enter().append("circle").attr(/* operate on new elements only */)
update_sel.attr(/* operate on old and new elements */)
update_sel.exit().remove() /* complete the enter-update-exit pattern */
From my code you can see I do enter() and then once again I add circle on node in a separate statement.
node = nodeGroup.selectAll(".node").data (nodes);
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
});
Adding circle should be within the scope of enter() otherwise it happens to all nodes not only the new nodes therefore it should be :
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag)
.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
});

D3 Collapsible Force Layout Mixing up Children and Parents

I'm working on the example located on jsfiddle, here.
It appears that I have everything structured properly, as the children are properly associated with their parents and the proper text is displayed.
The problems I've run into are as follows...
The main node (in the center) is not collapsible. The overall behavior of the graph is somewhat glitchy when compared to the example located here.
Colors do not change when nodes are collapsed, children of the parent node are displayed when the parent is collapsed. After several clicks on various nodes, children and parents seem to get switched.
My question is what section of code could be causing this and why?
Here's the code that I'm using to generate the chart. Data is missing, but is provided by the jsfiddle. Any help is appreciated, thanks in advance.
var width = 960,
height = 500,
root;
var force = d3.layout.force()
.charge(-220)
.size([width, height])
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link");
function update() {
var nodes = flatten(root);
var links = d3.layout.tree().links(nodes);
console.log(nodes);
// Restart the force layout.
force.nodes(nodes)
.links(links)
.linkDistance(55)
.start();
var link = svg.selectAll(".link")
.data(links, function(d) { return d.target.id; });
link.enter().append("line")
.attr("class", "link");
link.exit().remove();
var node = svg.selectAll("g.node")
.data(nodes)
var groups = node.enter().append("g")
.attr("class", "node")
.attr("id", function (d) {
return d.id
})
.on('click', click)
.call(force.drag);
groups.append("circle")
.attr("class","node")
.attr("x", -8)
.attr("y",-8)
.attr("r", function(d) { return d.children ? 4.5 : 10 })
.style("fill", color)
.on("click", click)
.call(force.drag);
groups.append("text")
.attr("dx", 12)
.attr("dy", "0.35em")
.style("font-size", "10px")
.style("color", "#000000")
.style("font-family", "Arial")
.text(function (d) {
console.log(d);
return d.name
});
node.exit().remove();
force.on("tick", function () {
link.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;
});
node.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
}
function tick() {
link.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; });
node.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
// Color leaf nodes orange, and packages white or blue.
function color(d) {
return d._children ? "#3182bd" // collapsed package
: d.children ? "#c6dbef" // expanded package
: "#fd8d3c"; // leaf node
}
// Toggle children on click.
function click(d) {
if (!d3.event.defaultPrevented) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update();
}
}
// Returns a list of all nodes under the root.
function flatten(root) {
var nodes = [], i = 0;
function recurse(node) {
if (node.children) node.children.forEach(recurse);
if (!node.id) node.id = ++i;
nodes.push(node);
}
recurse(root);
return nodes;
}
Ok, there are two things going on here.
First, by default, d3 uses the index of each datum as its ID (to determine when the item enters/exits the selection). This is your issue with parent/children moving around, when the element at index X is replaced by a new element, d3 thinks they are the same. You need to provide a function to return the id:
var node = svg.selectAll("g.node")
.data(nodes, function(d) { return d.id; });
Second, you're only setting the fill color of the circle when an item enters the selection. You should extract the bit that is assigning the style out of the enter() block, so it will be executed each time you call update().
node.selectAll('circle').style('fill', color);
I copied and hacked together your fiddle into plunker, because jsFiddle was running really slow for me:
http://plnkr.co/edit/7AJlQub6uCGQ3VSvq4pa?p=preview

D3 - use Transition when regrouping

experts out there,
i've got a little probem I seem not to be able to figure out by myself.
I have some data that are displayed as a graph. I get a file with the data and extract the required nodes to an array i called 'reqNodes'.
The main-node that is supposed to be 'the center' of all the displayed data is fixed, the others have a force layout applied to them.
What I struggle with at the moment: if the user clicks on any other node, this one becomes the center of the graph. he is fixed, the old one unfixed. Working so far. But I would like the new center node to transition to the center of the screen. At the moment all the nodes always stay where I clicked them, and so the graph moves with every new node i click, if i don't push it back to the center manually.
If you have any great idea to help me out, I'd be very glad!
greetz, Dave
function update() {
group.selectAll(".link").remove();
group.selectAll(".node").remove();
for(var count = 0; count < reqNodes.length; count++){
reqNodes[count].fixed = false;
}
var fixedNode = reqNodes[getNodesFinalIndex(mittelpunkt)];
fixedNode.fixed = true;
fixedNode.px = width/2;
fixedNode.py = height/2;
force
.nodes(reqNodes)
.links(reqLinks)
.start();
link = group.selectAll(".link")
.data(reqLinks);
link.enter().append("line")
.attr("class", "link")
.style("stroke", "#000")
.style("stroke-width", function(d) { return Math.sqrt(d.value)*1.2; });
node = group.selectAll(".node")
.data(reqNodes);
node.enter().append("circle")
.attr("class", "node")
.attr("r", 7)
.style("stroke", "black")
.style("fill", function(d) { return colorArray[d.group]; })
.call(force.drag);
for(var oo = 0; oo < group.selectAll(".node").length; oo++){
if(group.selectAll(".node")[oo].name = mittelpunkt){
console.log("node[0][oo]: ");
console.log(node[0][oo]);
node[0][oo].style.stroke = "red";
node[0][oo].style.strokeWidth = 4;
}
}
node.append("title")
.text(function(d) { return d.name; });
node.on("click", function(d) {
mittelpunkt = d.name;
paintIt();
});
node.append("text").attr("dx", 12).attr("dy", ".35em").attr("fill", "#aa0101").text(function(d) {
return d.name
});
force.on("tick", function() {
link.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; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
Hope i didn't forget something essential. If you miss something, just let me know!
Note:
var group = d3.select("#chart").append("svg").attr("width", width).attr("height", height).attr("id", 'networkBox').append('g');

Categories

Resources