d3.js custom layout exit() not working - javascript

I want to build a Windows Explorer like hierarchical visualization. As I want to compute the x and y coordinates manually, I created a custom layout based on the first example described here:
Custom layout in d3.js?
My layout function looks like this:
function myTreeLayout(data) {
var nodes = []; // or reuse data directly depending on layout
//load all nodes and their subnodes:
var coreelement=data;
coreelement.x=0;
coreelement.y=0;
positions(coreelement,0);
//nodes.push(coreelement); //core element
function child_recursion(element) {
nodes.push(element);
if (element.children!=null){
element.children.forEach(function(child) {
child_recursion(child);});
};
}
child_recursion(coreelement);
return nodes;
}
function positions(d,pos_y) { //pos_y is the target position (y) of the element
var sum_y;
sum_y=rowheight; //the sum of all vertical space used by that element
if (d.parent!=null)
{d.x=d.parent.x+10;}
else
{ d.x=0;}
d.y=pos_y;
if (d.children) {
d.children.forEach(function(child) {
child.parent=d;
sum_y+=positions(child,pos_y+sum_y);
});
}
return sum_y;
}
The computation of the coordinates works fine. I then bind the data using the following code:
d3.json("data/kdsf-neu.json", function(error, data) {
root = data;
root.x0 = 0;
root.y0 = 0;
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
root.children.forEach(collapse);
update(root);
});
function update(source) {
// Compute the new tree layout.
var nodes = myTreeLayout(root);
/*,links = tree.links(nodes);*/
// Update the nodes…
var node = vis.selectAll("g.node_coltree")
.data(nodes, function(d) {
return d.Nodeid;
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g").classed("g.node_coltree", true)
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
nodeEnter.append("svg:rect")
.attr("x", function(d) {
return 0;
})
.attr("y", function(d) {
return 0;
})
.attr("width", 10)
.attr("height", rowheight - 2)
.attr("class", function(d) {
var codearray = jQuery.makeArray(d.tags);
if ($.inArray(tags.Extended, codearray) >= 0) {
return 'erweiterungsteil_Fill';
} else if ($.inArray(tags.NotIncluded, codearray) >= 0) {
return 'nichtAufgenommen_Fill';
} else if ($.inArray(tags.Optional, codearray) >= 0) {
return 'optional_Fill';
} else if ($.inArray(tags.obligatorischWennVorhanden, codearray) >= 0) {
return 'obligatorisch_Fill';
} else if ($.inArray(tags.teilweiserForschungsbezug, codearray) >= 0) {
return 'pubSchale2_Fill';
} else if ($.inArray(tags.PublikationenSchale2, codearray) >= 0) {
return 'pubSchale2_Fill';
} else if ($.inArray(tags.Included, codearray) >= 0) {
return 'aufgenommen_Fill';
} else {
return "#FEFEFE";
}
})
.on("click", click)
.on("mouseover", function(d) {
updatedetails(d);
});
nodeEnter.append("text")
.attr("x", function(d) {
return 12;
})
.attr("y", function(d) {
return 7;
})
.text(function(d) {
return d.name;
})
.attr("dy", "0.35em")
.on("mouseover", function(d) {
updatedetails(d);
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
When I start the script, the elements are located at the right positions:
(As I am not allowed to post images, here a link:)
When I click on an element, however, the exit function does not seem to work:
https://www.dropbox.com/s/3phyu3tx9m13ydt/2.PNG?dl=0
After clicking on an element, the sub-elements are located at the appropriate target positions, but the old elements are not exiting.
I tried to stay close to the example for coltree, therefore I am also completely recalculating the whole tree after each click:
function update(source) {
// Compute the new tree layout.
var nodes = myTreeLayout(root);
I already checked the nodes element, it holds only the desired elements after the click. I therefore suspect, that there is some problem with the exit function and the custom layout.
Related questions:
My problem might be related to this question:
D3.js exit() not seeming to get updated information
Therefore, I followed the steps there:
I use a custom (externally computed single) index when calling data:
.data(nodes , function(d) { return d.Nodeid; });
I added the classed function when appending the node:
var nodeEnter = node.enter().append("g").classed("g.node_coltree",true)
Still, the elements stay in the graph - none are exiting.
Do I need to add something to the layout function, that d3 knows how to work with exiting elements? Or is something else wrong? Any help is highly appreciated.
EDIT: Here is the jsfiddle:
http://jsfiddle.net/MathiasRiechert/nhgejcy0/8/
When clicking on the root node, all sub-elements should disappear. Similarly, when opening a node, the elements should be moving. Both does not seem to happen.

You've got a fairly simple mistake in your code.
Here is an updated fiddle: http://jsfiddle.net/nhgejcy0/11/
The only difference is:
var nodeEnter = node.enter().append("g").classed("node_coltree", true)
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
Specifically, the first line was changed from:
var nodeEnter = node.enter().append("g").classed("g.node_coltree", true)
to:
var nodeEnter = node.enter().append("g").classed("node_coltree", true)
In your version, you were using classed(...) to add a class to your nodes of g.node_coltree, but you were selecting using .node_coltree, which didn't match, so your code just kept adding more and more g elements to the svg. Your enter selection contained a new g element for each item in your nodes array. This meant that your update and exit selections were always empty, resulting in nothing being removed.
I found this by inspecting the DOM and seeing that a new list of g elements was being appended every time a node was collapsed or expanded. If the selections were working properly, this wouldn't happen. It was then just a matter of tracking down whether the selection was wrong, or whether you were appending a different attribute when you created the nodes. In this case, it looks like the attribute was created incorrectly.

Related

D3.js v5 - Update nodes from another array of nodes

Each node I got is in g html tag which contains 2 more tags:
Text tag- representing the name of the node.
Circle tag - which is the node shape.
When I shifting from 1 set of nodes to another set of nodes in the HTML I see both text tags and circle tags of data from the old set of nodes and from the new set of node.
I tried to play with the exit().remove() method and with merge() method but nothing seems to fix my problem
Here is part of my update function which relevant to the nodes.
var node = d3.select('.nodes')
.selectAll('g.node');
//here is my selection between two array of nodes
node = node.data(show_bad_connections ? bad_nodes : nodes)
node.exit().remove();
node_enter = node.enter().append("g")
.attr("class", "node").merge(node);
node_enter.append("text")
.attr("class", "nodetext")
.attr("x", "0em")
.attr("y", 15)
.text(function (d) { return d.name });
node_enter.append("circle")
.style("fill", function (d) {
if (d.id == 0) { return "#0099ff" }
if (d.CF.includes(0)) { return "#00cc00" }
if (d.TSP > 0.5) { return "#ff9900" } else { return "#ff0000" }
})
.attr("r", r);
node_enter.on("mouseover", function (d) {
var g = d3.select(this); // The node
// The class is used to remove the additional text later
var info = g.append('text')
.classed('info', true)
.attr('dx', "0em")
.attr('dy', -10)
.text(function (d) {
if (d.id == 0) { return "id=0" }
else { return "id=" + d.id.toString() + ",TF=" + d.TF.toString() + ",AUA=" + d.AUA.toString() }
})
.style("font-size", "12px");
d3.selectAll('line.link')
.filter(function (l) {
return (d.id != 0 && (d.id == l.source.id || d.id == l.target.id));
})
.style("opacity", 1)
}).on("mouseout", function () {
d3.selectAll('line.link').style("opacity", 0.1)
// Remove the info text on mouse out.
d3.select(this).select('text.info').remove();
});
I expect to get g html tag with only the circle and text of new node.

d3 Force Graph Utilizing more than 1 JSON

I am not sure if this has been done (or can be done) since I have not seen any examples or questions regarding it but I will try to explain as best as I can.
I have a d3 force graph where I am trying to give it the functionality to "expand". Example: I have a JSON with
{
"nodes": [
{"name":"p1"},
{"name":"p2"},
{"name":"p3"},
{"name":"p4"}
],
"links": [
{"source":"p1","target":"p2"},
{"source":"p1","target":"p3"},
{"source":"p3","target":"p2"},
{"source":"p3","target":"p4"}
]}
So if a user selects node p3 and selects expand. It sends a request and we get a JSON back that can comes in with new nodes and links (but can also contain duplicates). ie,
{
"nodes": [
{"name":"p3"},
{"name":"p4"},
{"name":"p5"},
{"name":"p6"}
],
"links": [
{"source":"p3","target":"p4"},
{"source":"p4","target":"p5"},
{"source":"p4","target":"p6"}
]}
Since I wasn't sure if this could be done in d3 in the first place. I tested the functionality by just appending the new JSON data to the old JSON data (dupes and all). Now I assumed that d3 would check for duplicates already in the graph (like p3 to p4) but after appending, when I run the graph all p3 p4 p5 and p6 are just floating in space with no links even though I specified the links and it created p3 p4 node even though it already was there. (The initial graph with the 4 nodes still built and was linked properly).
So is the functionality possible to perform in d3? I have seen people who want to have multiple graphs on the screen but I am doing more of like an overlap/merge.
I have tried having my initial graph created then I use a test where I press a button and I read it in another JSON but it breaks or just doesn't create anything.
My code:
// Define the dimensions of the visualization.
var width = innerWidth,
height = innerHeight,
color = d3.scale.category20(),
root;
// Create an array logging what is connected to what
var linkedByIndex = { };
// Create the SVG container for the visualization and define its dimensions
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node"),
linkText;
// Mouse Event Variables
var selected_node = null,
selected_link = null,
mousedown_node = null,
mousedown_link = null,
mouseup_node = null;
// Create the force layout
var force = d3.layout.force()
.size([width, height])
.charge(-650)
.linkDistance(80);
var jsonStack = { };
var jsonCount = 0;
var jsonPath1 = "../../test/resources/cytoscape.json";
var jsonPath2 = "../../test/resources/cytoscapeexpand.json";
// Read in the JSON data.
d3.json(jsonPath1, function (error, json) {
// expands scope of json
jsonStack[jsonCount] = json;
root = jsonStack[jsonCount];
console.log("Successfully loaded" + json);
//console.log(JSON.stringify(root));
update();
jsonCount += 1;
});
d3.select('#expand').on("click", function() {
d3.json(jsonPath2, function(error, json) {
// expands scope of json
root = json
update();
});
});
function update() {
// sets the source and target to use id instead of index
root.edges.forEach(function(e) {
var sourceNode = root.nodes.filter(function(n) {
return n.data.id === e.data.source;
})[0],
targetNode = root.nodes.filter(function(n) {
return n.data.id === e.data.target;
})[0];
// push the EDGE attributes in the JSON to the edges array.
edges.push({
source: sourceNode,
target: targetNode,
label: e.data['label'],
color: e.data['color']
});
});
force
.nodes(root.nodes)
.links(edges)
.start();
link = link
.data(edges)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", 1.5);
node = node
.data(root.nodes)
.enter().append("g")
.attr("class", "node")
//.attr("fixed", function(d) { return d.fixed=true })
.call(force.drag)
.on('mouseover', connectedNodes)
.on('mouseleave', restore)
.on('dblclick', highlight);
node.append("circle").attr("r", 11);
node.style("fill", function(d) { return d.data['color'] }).select("circle").style("stroke", "black");
link.style("stroke", function(d) { return d.color });
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.style("fill", "black")
.text(function (d) { return d.data['label']; });
root.edges.forEach(function (d) {
linkedByIndex[d.data.source + "," + d.data.target] = 1;
});
resize();
window.addEventListener('resize', resize);
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 + ")"; });
});
}
// Highlighting of selected node.
function highlight(d) {
if (d.selected == false) {
d.selected = true;
return d3.select(this).style("fill", "yellow");
}
else {
d.selected = false;
return d3.select(this).style("fill", d.data['color']);
}
update();
}

Reloading (updating) d3.js force-directed graph holds onto old JSON data

So I have a d3.js force-directed graph that displays data from a JSON feed. When I click a node I requested an updated JSON feed based off the node that was clicked.
The JSON that is returned is correct. But what displays in the graph does not reflect what data is held in the JSON. I have a feeling the graph is holding onto previous graph data.
Heres a quick gif that should help visualise the issue.
Here is a JSFiddle to give you an idea of how to graph currently works.
And the Javascript on its own is at the bottom of this question.
In a bit more detail. When you click a node, it passes the word that node is associated with into a query string of a URL. I then run the d3.json using this new 'clicked' url and run an update function to recreate the graph.
So an example of how this is wrong. So if you go onto the JSFiddle and click on the node called 'piercingly' you will find that the next graph that is loaded doesn't even have the word 'piercingly' in it, and still has labels associated to bitter (the original search). Yet it if you change the variable at the top of the JS to 'piercingly' a different but correct graph is loaded.
The number of nodes is correct. But the label and other attributes in the full version (not the version on JSFiddle) are incorrect.
Any help would be much appreciated.
$wordToSearch = "bitter";
var w = 960,
h = 960,
node,
link,
root,
title;
var jsonURL = 'http://desolate-taiga-6759.herokuapp.com/word/basic/' + $wordToSearch;
d3.json(jsonURL, function(json) {
root = json.words[0]; //set root node
root.fixed = true;
root.x = w / 2;
root.y = h / 2 - 80;
update();
});
var force = d3.layout.force()
.on("tick", tick)
.charge(-700)
.gravity(0.1)
.friction(0.9)
.linkDistance(50)
.size([w, h]);
var svg = d3.select(".graph").append("svg")
.attr("width", w)
.attr("height", h);
//Update the graph
function update() {
var nodes = flatten(root),
links = d3.layout.tree().links(nodes);
// Restart the force layout.
force
.nodes(nodes)
.links(links)
.start();
// Update the links…
link = svg.selectAll("line.link")
.data(links, function(d) { return d.target.id; });
// Enter any new links.
link.enter().insert("svg: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; });
// Exit any old links.
link.exit().remove();
// Update the nodes…
node = svg.selectAll(".node")
.data(nodes);
var nodeE = node
.enter();
var nodeG = nodeE.append("g")
.attr("class", "node")
.call(force.drag);
nodeG.append("circle")
.attr("r", 10)
.on("click", click)
.style("fill", "red");
nodeG.append("text")
.attr("dy", 10 + 15)
.attr("text-anchor", "middle")
.text(function(d) { return d.word });
node.exit().remove();
}
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 + ")"; });
}
/***********************
*** CUSTOM FUNCTIONS ***
***********************/
//Request extended JSON objects when clicking a clickable node
function click(d) {
$wordClicked = d.word;
var jsonURL = 'http://desolate-taiga-6759.herokuapp.com/word/basic/' + $wordClicked;
console.log(jsonURL);
updateGraph(jsonURL);
}
// Returns a list of all nodes under the root.
function flatten(root) {
var nodes = [], i = 0;
function recurse(node) {
if (node.children) node.size = node.children.reduce(function(p, v) { return p + recurse(v); }, 0);
if (!node.id) node.id = ++i;
nodes.push(node);
return node.size;
}
root.size = recurse(root);
return nodes;
}
//Update graph with new extended JSON objects
function updateGraph(newURL) {
d3.json(newURL, function(json) {
root = json.words[0]; //set root node
root.fixed = true;
root.x = w / 2;
root.y = h / 2 - 80;
update();
});
}
function getUrlParameter(sParam)
{
var sPageURL = window.location.search.substring(1);
var sURLVariables = sPageURL.split('&');
for (var i = 0; i < sURLVariables.length; i++)
{
var sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] == sParam) {
return sParameterName[1];
}
}
}
EDIT: So I tried logging out the word when it is added to the text element. On first load all the words get logged as the get assigned to their respected text element. But when you click on the node, they don't. (Please see gif below). This is strange as I called the update function on click. So the word (in theory) should be fetched again for that node. But it doesnt.
It's quite hard to grasp on a phone but I think the reason is probably because it's getting confused about the new data. By default the data() function uses the index of the item to join to the DOM.
What you need to do instead is pass another function to your calls to data() which is described as a key function. Here you can probably just return the word.
.data(nodes, function(d) { return d.word; })
Take a look at the data function in the API docs https://github.com/mbostock/d3/wiki/Selections . I've had complex cases catch me out a couple of times where I missed a key function.

Multiple Force Layouts Cause Conflicts in Tick Functions

I'm attempting to put multiple D3 force layouts on a page at the same time. The number of force layouts is ideally variable, depending on the number of roots returned from a dynamic API. I have followed the answers on this question regarding multiple force layouts and have successfully put each layout in a separate div, in a separate svg.
However, the issue is twofold:
1) The svgs seem to be drawn at the same time, causing conflicts in the alpha cooling parameter (on "tick" of each graph). Thus, the only layout that is positioned the way it is intended is the last svg drawn on the page. The tick function contains code that shapes the force layout similar to a weeping willow tree, with the root node sitting on top and the children falling below it.
2) Setting a loop to iterate on the full results list from the API causes D3 to crash, and an error "Uncaught TypeError: Cannot read property 'textContent' of null."
I think the ideal solution would be to draw each force layout after the previous one has been successfully rendered, in a way that does not cause the alpha cooling parameters (on "tick") to conflict, or overloading the D3 library with too many instances of the force layout at once. Does someone have insight into this issue? Here is my code:
/* ... GET THE RESULTS FROM THE API ...*/
function handleRequest2(json) {
allroots = json[1]['data']['children'];
(function() {
var index = 0;
function LoopThrough() {
currentRoot = allroots[index];
if (index < allroots.length) {
/* DRAW THE GRAPH */
draw_graphs(currentRoot, index);
++index;
LoopThrough();
};
}
LoopThrough();
})();
}
//Force Layout Code
function draw_graphs(root, id) {
var root_id = "map-" + id.toString();
var force;
var vis;
var link;
var node;
var w = 980;
var h = 1000;
var k = 0;
// Create a separate div to house each SVG graph
div = document.createElement("div");
div.style.width = "980px";
div.style.height = "1000px";
div.style.cssFloat="left";
div.id = root_id;
$(div).addClass("chattermap-map");
// Append the div to the chart container
$('#chart').append(div);
force = d3.layout.force()
.size([w, h])
.charge(-250)
.gravity(0)
.on("tick", tick);
// Create the SVG and append it to the created div
vis = d3.select("#"+root_id)
.append("svg:svg")
.attr("width", w)
.attr("height", h)
.attr("id",root_id);
// Put the Reddit JSON in the correct format for the Force Layout
nodes = flatten(root),
links = optimize(d3.layout.tree().links(nodes));
// Calculations for the sizing of the nodes
avgNetPositive = getAvgNetPositive();
maxNetPositive = d3.max(netPositiveArray);
minNetPositive = d3.min(netPositiveArray);
// Create a logarithmic scale that sizes the nodes
radius = d3.scale.pow().exponent(.3).domain([minNetPositive,maxNetPositive]).range([5,30]);
// Fix the root node to the top of the svg
root.data.fixed = true;
root.data.x = w/2;
root.data.y = 50;
// Start the force layout.
force
.nodes(nodes)
.links(links)
.start();
// Update the links
link = vis.selectAll("line.link")
.data(links, function(d) { return d.target.id; });
// Enter any new links.
link.enter().insert("svg: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; });
// Exit any old links.
link.exit().remove();
// Update the nodes
node = vis.selectAll("circle.node")
.data(nodes, function(d) {return d.id; })
.style("fill", function(d) {
return '#2960b5';
});
// Enter any new nodes.
node.enter().append("svg:circle")
.attr("class", "node")
.attr("cx", function(d) {return d.x; })
.attr("cy", function(d) {return d.y; })
.attr("r", function(d) {
//Get the net positive reaction
var netPositive = d.ups - d.downs;
var relativePositivity = netPositive/avgNetPositive;
//Scale the radii based on the logarithmic scale defined earlier
return radius(netPositive);
})
.style("fill", function(d) {
return '#2960b5';
})
// Allow dragging on click
.call(force.drag);
// Exit any old nodes.
node.exit().remove();
//This will add the name of the author to the node HTML
node.append("author").text(function(d) {return d.author});
//Add the body of the comment to the node
node.append("comment").text(function(d) {return Encoder.htmlDecode(d.body_html)});
//Add the UNIX timestamp to the node
node.append("timestamp").text(function(d) {return moment.unix(d.created_utc).fromNow();})
//On load, assign the root node to the tooltip
numberOfNodes = node[0].length;
rootNode = d3.select(node[0][parseInt(numberOfNodes) - 1]);
rootNodeComment = rootNode.select("comment").text();
rootNodeAuthor = rootNode.select("author").text();
rootNodeTimestamp = rootNode.select("timestamp").text();
// Create the tooltip div for the comments
tooltip_div = d3.select("#"+root_id).append("div")
.attr("class", "tooltip")
.style("opacity", 1);
//Add the HTML to the tooltip for the root
tooltip_div .html("<span class='commentAuthor'>" + rootNodeAuthor + "</span><span class='bulletTimeAgo'>•</span><span class='timestamp'>" + rootNodeTimestamp + "</span><br>" + rootNodeComment)
//Position the tooltip based on the position of the current node, and it's size
.style("left", (rootNode.attr("cx") - (-rootNode.attr("r")) - (-9)) + "px")
.style("top", (rootNode.attr("cy") - 15) + "px");
node.on("mouseover", function() {
currentNode = d3.select(this);
currentTitle = currentNode.select("comment").text();
currentAuthor = currentNode.select("author").text();
currentTimestamp = currentNode.select("timestamp").text();
tooltip_div.transition()
.duration(200)
.style("opacity", 1);
// Add the HTML for all other tooltips on mouseover
tooltip_div .html("<span class='commentAuthor'>" + currentAuthor + "</span><span class='bulletTimeAgo'>•</span><span class='timestamp'>" + currentTimestamp + "</span><br>" + currentTitle)
//Position the tooltip based on the position of the current node, and it's size
.style("left", (currentNode.attr("cx") - (-currentNode.attr("r")) - (-9)) + "px")
.style("top", (currentNode.attr("cy") - 15) + "px");
});
// Fade out the tooltip on mouseout
node.on("mouseout", function(d) {
tooltip_div.transition()
.duration(500)
.style("opacity", 1);
});
// Optimize the JSON output of Reddit for D3
function flatten(root) {
var nodes = [], i = 0, j = 0;
function recurse(node) {
if (node['data']['replies'] != "" && node['kind'] != "more") {
node['data']['replies']['data']['children'].forEach(recurse);
}
if (node['kind'] !="more") {
//Add an ID value to the node starting at 1
node.data.id = ++i;
node.data.name = node.data.body;
//Put the replies in the key 'children' to work with the tree layout
if (node.data.replies != "") {
node.data.children = node.data.replies.data.children;
//Remove the extra 'data' layer for each child
for (j=0; j < node.data.children.length; j++) {
node.data.children[j] = node.data.children[j].data;
}
} else {
node.data.children = "";
}
var comment = node.data;
nodes.push(comment);
}
}
recurse(root);
return nodes;
}
// Optimize the JSON for use with Links
function optimize(linkArray) {
optimizedArray = [];
for (k=0; k < linkArray.length; k++) {
if(typeof linkArray[k].target.count == 'undefined') {
optimizedArray.push(linkArray[k]);
}
}
return optimizedArray;
}
// Get the average net positive upvotes for use in sizing
function getAvgNetPositive() {
var sum = 0;
netPositiveArray = []
//Select all the nodes
var allNodes = d3.selectAll(nodes)[0];
//For each node, get the net positive votes and add it to the sum
for (i=0; i < allNodes.length; i++) {
var netPositiveEach = allNodes[i]["ups"] - allNodes[i]["downs"];
sum += netPositiveEach;
netPositiveArray.push(netPositiveEach);
}
var avgNetPositive = sum/allNodes.length;
return avgNetPositive;
}
function tick(e) {
var kx = .4 * e.alpha, ky = 1.4 * e.alpha;
links.forEach(function(d, i) {
d.target.x += (d.source.x - d.target.x) * kx;
d.target.y += (d.source.y + 80 - d.target.y) * ky;
});
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; });
}
// // Remove the animation effect of the force layout
// while ((force.alpha() > 1e-2) && (k < 150)) {
// force.tick(),
// k = k + 1;
// }
}
Thanks in advance!
You should be able to make several force layouts work at the same time if you encapsulate them in their own namespace, e.g. through separate functions. You can however also do what you want by listening to the end event -- see the documentation. This way, you can "chain" the layouts, starting each one once the previous has finished.
Regarding the other error, it looks like this would be caused by incomplete/faulty data.

Adding new nodes to Force-directed layout

First question on Stack Overflow, so bear with me! I am new to d3.js, but have been consistently amazed by what others are able to accomplish with it... and almost as amazed by how little headway I've been able to make with it myself! Clearly I'm not grokking something, so I hope that the kind souls here can show me the light.
My intention is to make a reusable javascript function which simply does the following:
Creates a blank force-directed graph in a specified DOM element
Allows you to add and delete labeled, image-bearing nodes to that graph, specifying connections between them
I've taken http://bl.ocks.org/950642 as a starting point, since that's essentially the kind of layout I want to be able to create:
Here's what my code looks like:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="underscore-min.js"></script>
<script type="text/javascript" src="d3.v2.min.js"></script>
<style type="text/css">
.link { stroke: #ccc; }
.nodetext { pointer-events: none; font: 10px sans-serif; }
body { width:100%; height:100%; margin:none; padding:none; }
#graph { width:500px;height:500px; border:3px solid black;border-radius:12px; margin:auto; }
</style>
</head>
<body>
<div id="graph"></div>
</body>
<script type="text/javascript">
function myGraph(el) {
// Initialise the graph object
var graph = this.graph = {
"nodes":[{"name":"Cause"},{"name":"Effect"}],
"links":[{"source":0,"target":1}]
};
// Add and remove elements on the graph object
this.addNode = function (name) {
graph["nodes"].push({"name":name});
update();
}
this.removeNode = function (name) {
graph["nodes"] = _.filter(graph["nodes"], function(node) {return (node["name"] != name)});
graph["links"] = _.filter(graph["links"], function(link) {return ((link["source"]["name"] != name)&&(link["target"]["name"] != name))});
update();
}
var findNode = function (name) {
for (var i in graph["nodes"]) if (graph["nodes"][i]["name"] === name) return graph["nodes"][i];
}
this.addLink = function (source, target) {
graph["links"].push({"source":findNode(source),"target":findNode(target)});
update();
}
// set up the D3 visualisation in the specified element
var w = $(el).innerWidth(),
h = $(el).innerHeight();
var vis = d3.select(el).append("svg:svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.nodes(graph.nodes)
.links(graph.links)
.gravity(.05)
.distance(100)
.charge(-100)
.size([w, h]);
var update = function () {
var link = vis.selectAll("line.link")
.data(graph.links);
link.enter().insert("line")
.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; });
link.exit().remove();
var node = vis.selectAll("g.node")
.data(graph.nodes);
node.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("image")
.attr("class", "circle")
.attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
.attr("x", "-8px")
.attr("y", "-8px")
.attr("width", "16px")
.attr("height", "16px");
node.append("text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(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 + ")"; });
});
// Restart the force layout.
force
.nodes(graph.nodes)
.links(graph.links)
.start();
}
// Make it all go
update();
}
graph = new myGraph("#graph");
// These are the sort of commands I want to be able to give the object.
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");
</script>
</html>
Every time I add a new node, it re-labels all of the existing nodes; these pile on top of each other and things start to get ugly. I understand why this is: because when I call the update() function function upon adding a new node, it does a node.append(...) to the entire data set. I can't figure out how to do this for only the node I'm adding... and I can only apparently use node.enter() to create a single new element, so that doesn't work for the additional elements I need bound to the node. How can I fix this?
Thank you for any guidance that you're able to give on any of this issue!
Edited because I quickly fixed a source of several other bugs that were previously mentioned
After many long hours of being unable to get this working, I finally stumbled across a demo that I don't think is linked any of the documentation: http://bl.ocks.org/1095795:
This demo contained the keys which finally helped me crack the problem.
Adding multiple objects on an enter() can be done by assigning the enter() to a variable, and then appending to that. This makes sense. The second critical part is that the node and link arrays must be based on the force() -- otherwise the graph and model will go out of synch as nodes are deleted and added.
This is because if a new array is constructed instead, it will lack the following attributes:
index - the zero-based index of the node within the nodes array.
x - the x-coordinate of the current node position.
y - the y-coordinate of the current node position.
px - the x-coordinate of the previous node position.
py - the y-coordinate of the previous node position.
fixed - a boolean indicating whether node position is locked.
weight - the node weight; the number of associated links.
These attributes are not strictly needed for the call to force.nodes(), but if these are not present, then they would be randomly initialised by force.start() on the first call.
If anybody is curious, the working code looks like this:
<script type="text/javascript">
function myGraph(el) {
// Add and remove elements on the graph object
this.addNode = function (id) {
nodes.push({"id":id});
update();
}
this.removeNode = function (id) {
var i = 0;
var n = findNode(id);
while (i < links.length) {
if ((links[i]['source'] === n)||(links[i]['target'] == n)) links.splice(i,1);
else i++;
}
var index = findNodeIndex(id);
if(index !== undefined) {
nodes.splice(index, 1);
update();
}
}
this.addLink = function (sourceId, targetId) {
var sourceNode = findNode(sourceId);
var targetNode = findNode(targetId);
if((sourceNode !== undefined) && (targetNode !== undefined)) {
links.push({"source": sourceNode, "target": targetNode});
update();
}
}
var findNode = function (id) {
for (var i=0; i < nodes.length; i++) {
if (nodes[i].id === id)
return nodes[i]
};
}
var findNodeIndex = function (id) {
for (var i=0; i < nodes.length; i++) {
if (nodes[i].id === id)
return i
};
}
// set up the D3 visualisation in the specified element
var w = $(el).innerWidth(),
h = $(el).innerHeight();
var vis = this.vis = d3.select(el).append("svg:svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([w, h]);
var nodes = force.nodes(),
links = force.links();
var update = function () {
var link = vis.selectAll("line.link")
.data(links, function(d) { return d.source.id + "-" + d.target.id; });
link.enter().insert("line")
.attr("class", "link");
link.exit().remove();
var node = vis.selectAll("g.node")
.data(nodes, function(d) { return d.id;});
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(force.drag);
nodeEnter.append("image")
.attr("class", "circle")
.attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
.attr("x", "-8px")
.attr("y", "-8px")
.attr("width", "16px")
.attr("height", "16px");
nodeEnter.append("text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {return d.id});
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 + ")"; });
});
// Restart the force layout.
force.start();
}
// Make it all go
update();
}
graph = new myGraph("#graph");
// You can do this from the console as much as you like...
graph.addNode("Cause");
graph.addNode("Effect");
graph.addLink("Cause", "Effect");
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");
</script>

Categories