I am working with force layout and tree hierarchy.
It almost works and its adding nodes and links, but they are not visible.
Probably I am missing something trivial, but I have spent too many hours on this...
Any help greatly appreciated.
In this mock-up, after click, third node should be add to root.
var width = 960,
height = 500,
root;
var force = d3.layout.force()
.linkDistance(80)
.charge(-320)
.gravity(0.05)
.size([width, height])
.on("tick", tick);
var svg = d3.select("#myviz").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
var mydata = '{ "name": "start_bubble", "children": [ { "name": "child_O1", "size": 31812},{ "name": "child_O2", "size": 31812}]}';
var myadd = '{"name": "new_child", "size": 100}';
root = JSON.parse( mydata);
myaddparsed = JSON.parse( myadd);
update();
force.start();
function update() {
var nodes = flatten(root),
links = d3.layout.tree().links(nodes);
// Restart the force layout.
force
.nodes(nodes)
.links(links)
.start();
// Update links.
link = link.data(links, function(d) { return d.target.id; });
link.exit().remove();
link.enter().insert("line", ".node")
.style("stroke-width",2)
.attr("class", "link");
// Update nodes.
node = node.data(nodes, function(d) { return d.id; });
node.exit().remove();
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.on("click", click)
.call(force.drag);
nodeEnter.append("circle")
.attr("r", function(d) { return Math.sqrt(d.size) / 5 || 14.5; });
nodeEnter.append("text")
.attr("dy", "2.3em")
.text(function(d) { return d.name; });
node.select("circle")
.style("fill", color);
force.start();
}
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 + ")"; });
}
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) return; // ignore drag
if (d.children) {
// d._children = d.children; remove for testing
// d.children = null; removed for testing
root.children[2]=myaddparsed; //add third node
} else {
// d.children = d._children; removed for tesing
// d._children = null; removed for testing
root.children[2]=myaddparsed; //add third node
}
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;
}
http://jsfiddle.net/jo636df/o81xxmjm/
You come to stackoverflow, and you say: "I index my data with the value of i incremented in recurse." But you don't maintain the value between calls to flatten. You don't offer uniqueness. You don't even think to tell D3 that you have new data. Instead, you reset i to 0, immediately increment it by one with ++i, set it on your new element and you ask D3 to figure out that your new element is indeed new when you already have another element with id equal to 1.
You need to ensure your ids are indeed unique. As a quick fix you can pull i to the outer context to avoid resetting it to zero. Here's a fiddle.
Related
I'm trying to combine two different force directed graphs.
One is dynamic force directed graph (code from here) where I can set the thickness of the lines between nodes (and the data is defined inline); the other is collapsible force directed graph (code from a SO answer here, that uses non-tree data) where I can click the nodes to hide the children (and the data is defined from a JSON file).
I thought this should be clear enough, but I keep having problems. The JS console said that the propertyforce is undefined even though I have set its variable.
Here is the Plunker: http://plnkr.co/edit/MtOB9PGnXlNwlNj6P5NY?p=preview
And here is the collapsible graph part (full code is in Plunker)
// Combined script //
var root,
force = d3.layout.force()
.size([width, height])
.on("tick", tick);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
d3.json("data.json", function(json) {
root = json;
//Give nodes ids and initialize variables
for(var i=0; i<root.nodes.length; i++) {
var node = root.nodes[i];
node.id = i;
node.collapsing = 0;
node.collapsed = false;
}
//Give links ids and initialize variables
for(var i=0; i<root.links.length; i++) {
var link = root.links[i];
link.source = root.nodes[link.source];
link.target = root.nodes[link.target];
link.id = i;
}
update();
});
function update() {
//Keep only the visible nodes
var nodes = root.nodes.filter(function(d) {
return d.collapsing == 0;
});
var links = root.links;
//Keep only the visible links
links = root.links.filter(function(d) {
return d.source.collapsing == 0 && d.target.collapsing == 0;
});
force
.nodes(nodes)
.links(links)
.start();
// Update the links…
link = link.data(links, function(d) { return d.id; });
// Exit any old links.
link.exit().remove();
// Enter any new links.
link.enter().insert("line", ".node")
.attr("class", "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; })
.attr("marker-end", "url(#arrow)");
// Update the nodes…
node = node.data(nodes, function(d){ return d.id; }).style("fill", color);
// Exit any old nodes.
node.exit().remove();
// Enter any new nodes.
node.enter().append("circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) { return Math.sqrt(d.size) / 10 || 4.5; })
.style("fill", color)
.on("click", click)
.call(force.drag);
}
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// Toggle children on click.
function click(d) {
if (!d3.event.defaultPrevented) {
//check if link is from this node, and if so, collapse
root.links.forEach(function(l) {
if(l.source.id == d.id) {
if(d.collapsed){
l.target.collapsing--;
} else {
l.target.collapsing++;
}
}
});
d.collapsed = !d.collapsed;
}
update();
}
// Combined end //
Without combining the two codes, it's working fine, as shown in this fiddle: https://jsfiddle.net/Lrrepn0c/
Any ideas?
I am trying to add a threshold to a force directed graph, where I will only include edges between vertices that that are above some threshold I store in a map. My slider is partly working, and the edges successfully get removed.
However, after the edges are removed, the graph stops animating, and there is an error in the console when calling force.start(). Do I really need to add unique ids? In the JSFiddle I linked, he does not do that, and his slider works with no problems.
Thanks!
There are multiple answers on StackOverflow to similar questions, and I have used them to fix my obvious errors (such as d3.js: "Cannot read property 'weight' of undefined" when manually defining both nodes and links for force layout), but I am down to this one. I am using this example: http://jsfiddle.net/simonraper/TdHgx/?utm_source=website&utm_medium=embed&utm_campaign=TdHgx from this website: http://www.coppelia.io/2014/07/an-a-to-z-of-extra-features-for-the-d3-force-layout/
I will send the relevant JS.
function draw(occurence) {
var ratio = window.devicePixelRatio || 1;
var width = Math.min(700,0.8*$(window).width()), height = 700;
var color = d3.scale.category20();
var svg = d3.select("#" + occurence).append("svg")
.attr("width", width)
.attr("height", height)
.attr("id", "#" + occurence + "svg");
d3.json(occurence + ".json", function(error, graph) {
if (error) throw error;
var force = d3.layout.force()
.charge(-120)
.linkDistance(50)
.size([width, height]);
force
.nodes(graph.nodes)
.links(graph.links)
.start();
if (occurence == "occurrences3") {
console.log(graph.nodes);
console.log(graph.links);
}
var graphRec=JSON.parse(JSON.stringify(graph)); //Add this line
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
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; });
});
node.on("dblclick", function(d) {
$("#uncuratedGraphsModal").modal();
var d = d3.select(this).node().__data__;
var name = d.name;
$("#uncuratedGraphsModalHeader").text("Node " + name);
$("#uncuratedGraphsModalBody").empty();
var edge_by_person = edge_by_person_per_threshold[occurence + "edge_by_person"];
var edges = edge_by_person[name];
edges.sort(function(a,b) {
return a.dest - b.dest;
});
$.each(edges, function(edge) {
var new_edge = $("<li>");
new_edge.text("Neighbor: " + edges[edge].dest + ", Token: " + edges[edge].token);
$("#uncuratedGraphsModalBody").append(new_edge);
});
});
$("#" + occurence + "thresholdSlider").on('input', function(thresh) {
threshold($("#" + occurence + "thresholdSlider").val());
});
//adjust threshold
function threshold(thresh) {
var edge_by_person = edge_by_person_per_threshold_unweighted[occurence + "edge_by_person_unweighted"];
graph.links.splice(0, graph.links.length);
for (var i = 0; i < graphRec.links.length; i++) {
var source = graphRec.links[i].source.name;
var dest = graphRec.links[i].target.name;
var value = -1;
var edges = edge_by_person[source];
var found = false;
for (var edge = 0; !found && edge < edges.length; edge++) {
if (dest == edges[edge].dest) {
value = edges[edge].token;
found = true;
}
}
if (value >= thresh) {
graph.links.push({
source: graphRec.links[i].source.name,
target: graphRec.links[i].target.name,
value: graphRec.links[i].value
});
}
}
restart();
}
//Restart the visualisation after any node and link changes
function restart() {
link = link.data(graph.links);
link.exit().remove();
link.enter().insert("line", ".node").attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
node = node.data(graph.nodes);
node.enter().insert("circle", ".cursor").attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
force
.nodes(node)
.links(link)
.start();
}
});
}
This is the JS that he added from the JsFiddle I linked:
This is my understanding: If you have the same nodes, and you just remove a few edges from graph.links, can't you just do a force.start()? When I directly copy his restart() function, all links are broken, and none get redrawn, even though doing a console.log(graph.links) shows the correct links to-redraw.
I believe that it's talking to the wrong nodes at this point, maybe the wrong SVG. I have multiple SVGs on the same page.
As I already had some memory leak issue on some other projects, I documented myself on how to release d3.js event listeners memory.
I found that assigning a null value was releasing the memory source 1 source 2:
svg.selectAll(".node").on('mouseover',null);
So before applying this solution to my code I did a first test to see how much memory was leaking.
I load the graph a first time and do a garbage collection.
Then several times: open a new simple view, come back to the graph and do a garbage collection.
The Listeners and the Nodes already fallback to the initial value (754 and 1901). But I haven't applied the solution yet!
What is happening here, does D3 or AngularJS already do the cleanup for me ? Should I still apply the my solution on the $destroy event ?
The directive code:
'use strict';
angular.module("appDirectives", []).directive("myGraph", function() {
return {
restrict: 'E',
scope: {
data: '='
},
link: function (scope, element, attrs) {
//values from controller
var links = scope.data.links;
var nodes = scope.data.nodes;
//force graph size, affect the gravitational center and initial random position
var width = 600,
height = 600;
//force graph initialisation
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(60)
.charge(-400)
.gravity(0.4);
//append svg element and width height
var svg = d3.select(element[0]).append("svg")
.attr("viewBox", "0 0 " + width + " " + height )
.attr("preserveAspectRatio", "xMinYMin");
//add links
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link")
.on("mouseover", function(d) {
scope.data.info = d.source.name+" | "+d.target.name;
scope.$apply();
});
//add nodes
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", function(d) { return "node "+d.type+" "+d.status; })
.on("mouseover", function(d) {
scope.data.info = d.type+" "+d.name+" <br/> "+d.status;
scope.$apply();
})
.call(force.drag);
//display node name on mouseover
node.append("title")
.text(function(d) { return d.name; });
// draw typeA as circle
var gateway = svg.selectAll(".typeA");
gateway.append("circle")
.attr("r", 6);
//draw typeB as square
var switche = svg.selectAll(".typeB");
switche.append("path").attr("d", d3.svg.symbol()
.type(function(d) { return 'square'; })
.size(100));
//draw typeC as triangle
var ap = svg.selectAll(".typeC");
ap.append("path").attr("d", d3.svg.symbol()
.type(function(d) { return 'triangle-up'; }));
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 + ")"; });
}
//use timeout to allow the rest of the page to load first
setTimeout(function() {
// pre render the graph
force.start();
for (var i = 100 ; i > 0; --i) force.tick();
force.stop();
//render the graph
force
.on("tick", tick)
.start();
}, 10);
}
};
});
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
I've been working with d3 for a while now in an attempt to create an interactive ecosystem explorer tool that maps out relationships between species. Recently I've tried adding a feature that lets users show or hide species (nodes) in the force directed graph. I've tried following other examples and although the code works - it only works inconsistently.
For some reason, when I add back a node, it sometimes isn't visible. The graph moves as if the node is added but then it doesn't show up. I have the feeling that it is adding it but then the node is being hidden again in the force.on("tick") code but have no idea why. I've posted the relevant code below and would really appreciate any ideas! The toggleNode function determines whether a node should be shown or hidden - basically just splicing or adding to the nodes array. I keep the data in an array called dataset that stores a flag to indicate whether a node is visible or not.
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([w, h]);
var nodes = force.nodes(), links = force.links(); // arrays to hold data
var vis = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h);
force.on("tick", function() {
vis.selectAll("circle.node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
vis.selectAll("line.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; });
});
function restart() {
// UPDATE nodes
var node = vis.selectAll("circle.node")
.data(nodes, function(d) { return d.id;});
// ENTER new nodes
var nodeEnter = node.enter().append("svg:circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.style("fill", function(d) { return groupColors[(d.group)-1]; })
.style("stroke","fff")
.style("stroke-width",1)
.on("mousedown", function(d) {
clickNode(d);
})
.call(force.drag);
// REMOVE deleted nodes
var nodeExit = node.exit().remove();
force.start();
}
// Add the nodes and links to the vis
function createVis(json) {
dataset = json; // store data in global
for (var i = 0; i < dataset['nodes'].length; i++) {
// fill node info
nodes.push(dataset['nodes'][i]);
}
restart();
}
// Remove node and associated links.
function toggleNode(nodeKey,eol_id) {
console.log(nodeKey + ': ' + eol_id);
var tLabel; // value for toggle label
if ( dataset['nodes'][nodeKey]['isHidden'] == 0 ) {
// node is visible, so hide it
tLabel = 'Show';
for( var k=0; k<nodes.length; k++ ) {
if ( nodes[k]['eol_id'] == eol_id ) {
nodes.splice(k, 1); // remove this node
break;
}
}
dataset['nodes'][nodeKey]['isHidden'] = 1;
console.log('node removed: ' + nodeKey);
} else {
dataset['nodes'][nodeKey]['isHidden'] = 0;
nodes.push(dataset['nodes'][nodeKey]);
tLabel = 'Hide';
}
$('#primary_title_toggle').html(' ' + tLabel + '<br>');
restart();
}