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

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.

Related

Circle Pack Layout D3 - remove add nodes on new data

I have a situation where I want users to browse 3 different data sets using circle pack layout. I am using Bostock's Zoomable circle packing.
I have added 3 options additionally which loads data and creates new nodes. Here chartid is passed from these elements.
function changeDataSet(chartid)
{
console.log(chartid);
//console.log(nodes);
//add new chart data depending on the selected option
if(chartid === "plevels")
{
root = JSON.parse(newKmap_slevels);
focus = root;
nodes = pack.nodes(root);
}
else if (chartid === "pduration")
{
root = JSON.parse(newKmap_sduration);
focus = root;
nodes = pack.nodes(root);
}
else
{
root = JSON.parse(newKmap_stype);
focus = root;
nodes = pack.nodes(root);
}
refresh();
}
Then I have the refresh function, but I am not understnading how to remove existting nodes, and add new ones based on the new data set. Showing some transition animations would be nice also.
Currently I am trying to remove and recreate the initial elements, but the chart goes blank when I do that.
var refresh = function() {
//var nodes = pack.nodes(root);
var duration = 10;
console.log(nodes);
d3.select("#conf_knowledge_map").selectAll("g")
.remove();
svg
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function(d) { return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root"; })
.style("fill", function(d) { return d.children ? color(d.depth) : null; })
.on("click", function(d) {
if (focus !== d) zoom(d), d3.event.stopPropagation();
});
text = svg.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("class", "label")
.style("fill-opacity", function(d) { return d.parent === root ? 1 : 0; })
.style("display", function(d) { return d.parent === root ? "inline" : "none"; })
.text(function(d) {
//console.log(d);
if( d.size )
{
return d.name + ":" + d.size;
}
else
return d.name;
});
}
So my question is how can I remove and then create new nodes on click?
UPDATE
I was able to remove all nodes and add the new nodes based on the new data, but now on click to zoom the layout is all messed up. The transform function is not applied to the new nodes somehow.
var refresh = function() {
svg.selectAll(".node").remove();
svg.selectAll(".label").remove();
var nodes = pack.nodes(root);
focus = root;
circle = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("class", function(d) { return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root"; })
.attr("r", function(d) { return d.r;})
.attr("cx", function(d) {
console.log(d);
// if(d.depth === 0)
// return 0;
// else
// return d.x - (diameter/2);
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
// .attr("transform", "translate(" + "x" + "," + "y" + ")")
.style("fill", function(d) { return d.children ? color(d.depth) : null; })
.on("click", function(d) {
// d.x = d.x - (diameter/2);
// d.y = d.y - (diameter/2);
if (focus !== d) zoom(d), d3.event.stopPropagation();
});
text = svg.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("class", "label")
.attr("x", function(d) {return d.x - (diameter/2);})
.attr("y", function(d) {return d.y - (diameter/2);})
.style("fill-opacity", function(d) { return d.parent === root ? 1 : 0; })
.style("display", function(d) { return d.parent === root ? "inline" : "none"; })
// .attr("transform", "translate(" + "x" + "," + "y" + ")")
.text(function(d) {
//console.log(d);
if( d.size )
{
return d.name + ":" + d.size;
}
else
return d.name;
});
}
How can I transform the new nodes to be in place and for the zoom to work properly?
UPDATE 2
Now the transform is working properly after attaching 'g' element to the circles, and showing all the nodes and text correctly. The only problem now is that the zoom does not work when I click on the circles!!
circle = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("class", function(d) { return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root"; })
.attr("r", function(d) { return d.r;})
.attr("cx", function(d) {
if(d.depth === 0)
return 0;
else
return d.x - (diameter/2);
})
.attr("cy", function(d) {
if(d.depth === 0)
return 0;
else
return d.y - (diameter/2);
})
.style("fill", function(d) { return d.children ? color(d.depth) : null;})
.on("click", function(d) {
if (focus !== d) zoom(d); d3.event.stopPropagation();
})
.append("g")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
;
How to make the zoom work??
It so happens that I was making the mistake of simultaneously creating three independent circle pack layouts, with mixed statements one after the other. This was problematic as when you have to select svg sections, different elements were all getting selected and wrongly attached with different events.
So I decided to separate the three implementations and create the three layouts one after the other, only after each one finished. This way I kept the section selection and attaching events etc, separate and then load them as and when required.

Draw text on svg path element with d3.js

I am trying to modify this force directed graph, and in particular what I'd like to do is to draw text on each graph edge. I tried the following:
// add new links
path.enter().append('svg:path')
.attr('class', 'link')
.classed('selected', function(d) { return d === selected_link; })
.style('marker-start', function(d) { return d.left ? 'url(#start-arrow)' : ''; })
.style('marker-end', function(d) { return d.right ? 'url(#end-arrow)' : ''; })
.on('mousedown', function(d) {
if(d3.event.ctrlKey) return;
// select link
mousedown_link = d;
if(mousedown_link === selected_link) selected_link = null;
else selected_link = mousedown_link;
selected_node = null;
restart();
})
.append('svg:text').attr('x', 0).attr('y', 4)
.attr('class', 'id').text(function(d) { console.log(d.id); return d.id; })
Only the last line is my modification. I also added an id property to the link objects so the code I'm working with is not exactly what I've linked to. The log statement is printing as expected but no text or error messages appear. How can I append text right in the middle of each link?
You will need to use the svg group <g>, that will let you append elements inside.
var new_paths = path.enter().append('g'),
links = new_paths.append('svg:path').attr('class', 'link');
Be free to add whatever style or behaviour to links as you wish.
Then, for the labels, you will append again to the group:
var labels = new_paths.append('svg:text').attr('class', 'label');
And again you can add whatever you want to labels.
When you define the force, you will need to select the links and labels, wich will be something like this:
force.on("tick", function() {
links.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; });
labels.attr("transform",function(d){
return
"translate("+
(0.5*(d.source.x+d.target.x))+
","+
(0.5*(d.source.y+d.target.y))+
")";
});
});
Here is a demo fiddle http://jsfiddle.net/k2oyef30/
Update
Sorry, I didn't follow your example before. Here is your particular solution.
Change path to be defined as:
// handles to link and node element groups
var path = svg.append('svg:g').selectAll('.path'),
circle = svg.append('svg:g').selectAll('g');
Inside function tick() { you have to select the .links or the .id
path.select('.link').attr('d', function(d) {
and manage the new element:
path.select('.id').attr("transform",function(d){
return "translate("+
(0.5*(d.source.x+d.target.x))+
","+
(0.5*(d.source.y+d.target.y))+
")";
});
Inside function restart() { you have to create a <g> element, for example with class path, and append the elements into it.
// add new links
var newPaths = path.enter().append('svg:g').attr('class','path');
newPaths.append('svg:path')
.attr('class', 'link')
.classed('selected', function(d) { return d === selected_link; })
.style('marker-start', function(d) { return d.left ? 'url(#start-arrow)' : ''; })
.style('marker-end', function(d) { return d.right ? 'url(#end-arrow)' : ''; })
.on('mousedown', function(d) {
if(d3.event.ctrlKey) return;
// select link
mousedown_link = d;
if(mousedown_link === selected_link) selected_link = null;
else selected_link = mousedown_link;
selected_node = null;
restart();
});
newPaths.append('svg:text').attr('x', 0).attr('y', 4)
.attr('class', 'id').text(function(d) { return d.id; });
The problem with the example definition was that it already selected the <path> element making you unable to select the new <g class="path">

Removing two nodes from a Force Layout fails while one succeeds

I'm trying to remove some nodes from a Force Layout.
The first part of the process is selecting one or more nodes. That's done with the click handler:
var nodeg = node.enter().append("g")
.attr("class", "node")
.on('click', function (n) {
if (n.dragging === true) {
return;
}
// select the clicked node
n.selected = !n.selected;
d3.select(this)
.transition()
.duration(500)
.ease('bounce')
.attr('fill', getNodeBackground(n))
.attr('transform', getNodeTransform(n));
})
.call(drag);
Note that I'm setting selected to true.
Next, if the user presses the delete key I remove the selected nodes:
function removeSelectedNodes() {
if (!confirm('Are you sure you want to delete the selected relationships?')) {
return;
}
var firstIndex = -1;
d3.selectAll('.node')
.each(function (n, i) {
if (n.selected !== true) {
return;
}
n.data.remove = true;
n.data.index = i;
if (firstIndex === -1) {
firstIndex = i;
}
});
var offset = 0;
_.each(_.where(scope.nodes, {data: {remove: true}}), function (n) {
var removeAt = n.index;
if (n.index > firstIndex) {
removeAt = n.index - 1 - offset;
offset++;
}
scope.nodes.splice(removeAt, 1);
scope.links.splice(removeAt - 1, 1);
});
renderGraph();
}
The entire renderGraph function looks like this:
function renderGraph() {
force
.nodes(scope.nodes)
.links(scope.links)
.start();
var link = svg.selectAll(".link")
.data(scope.links);
link.exit().remove();
link.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(scope.nodes);
node.exit().remove();
var nodeg = node.enter().append("g")
.attr("class", "node")
.on('click', function (n) {
if (n.dragging === true) {
return;
}
// select the clicked node
n.selected = !n.selected;
d3.select(this)
.transition()
.duration(500)
.ease('bounce')
.attr('fill', getNodeBackground(n))
.attr('transform', getNodeTransform(n));
})
.call(drag);
nodeg.append("image")
.attr("xlink:href", function (d) {
return d.avatar || 'https://github.com/favicon.ico'
})
.attr("x", -56)
.attr("y", -8)
.attr("width", 64)
.attr("height", 64);
nodeg.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.attr('class', 'name')
.text(function (d) {
return d.displayName;
});
nodeg.append("text")
.attr("dx", 12)
.attr("dy", "1.35em")
.text(function (d) {
return d.relationship;
});
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 getNodeTransform(d);
});
});
}
This is where it starts going south. If I select one node, the graph is rendered correctly after splicing the node. However, if I remove two nodes the graph ends up persisting the first removed node (by index).
Let's say the first node (by index) was 'Bob' and the second node was 'Bill'. The second node will be removed, but the first one will persist. Interestingly, another one of the nodes, the current last node (by index), will be gone instead.
NOTE: the array looks good. The nodes I wanted removed are gone, and the remaining ones are correct.
What did I do wrong here?
UPDATE: I've tried not setting nodes and links after removing nodes, and just calling start:
force.start()
This didn't work.
The answer, thanks to Lars again, was to add a key function to the links and nodes:
var link = svg.selectAll(".link")
.data(scope.links, function (d) {
return d.source.data._id + '|' + d.target.data._id;
});
var node = svg.selectAll(".node")
.data(scope.nodes, function (d) {
return d.data._id;
});
Thanks Lars!

d3.js custom layout exit() not working

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.

Converting only certain nodes in D3 Sankey chart from rectangle to circle

I would like to reproduce the process from D3 Sankey chart using circle node instead of rectangle node, however, I would like to select only certain nodes to change from rectangles to circles.
For example, in this jsfiddle used in the example, how would you only select Node 4 and Node 7 to be converted to a circle?
I updated your fiddle.
Basically you just need some way to select the nodes that you want to make different. I used unique classname so that you can style them with CSS as well. I didn't feel like writing the code to select just 4 and 7 (I'm lazy) so I just selected all of the even nodes instead.
// add in the nodes
var node = svg.append("g").selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", function (d, i) { return i % 2 ? "node rect" : "node circle";
})
Then you can use that to select the nodes and add circles instead of rectangles.
svg.selectAll(".node.circle").append("circle")
.attr("r", sankey.nodeWidth() / 2)
.attr("cy", function(d) { return d.dy/2; })
.attr("cx", sankey.nodeWidth() / 2)
.style("fill", function (d) {
There is also another similar approach, illustrated in the following jsfiddle.
I started from this fiddle (from another SO question that you merntioned)), where all nodes had already been converted to circles:
Then I modified existing and added some new code that involves filtering during creation of circles:
// add the circles for "node4" and "node7"
node
.filter(function(d){ return (d.name == "node4") || (d.name == "node7"); })
.append("circle")
.attr("cx", sankey.nodeWidth()/2)
.attr("cy", function (d) {
return d.dy/2;
})
.attr("r", function (d) {
return Math.sqrt(d.dy);
})
.style("fill", function (d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("fill-opacity", ".9")
.style("shape-rendering", "crispEdges")
.style("stroke", function (d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
// add the rectangles for the rest of the nodes
node
.filter(function(d){ return !((d.name == "node4") || (d.name == "node7")); })
.append("rect")
.attr("y", function (d) {
return d.dy/2 - Math.sqrt(d.dy)/2;
})
.attr("height", function (d) {
return Math.sqrt(d.dy);
})
.attr("width", sankey.nodeWidth())
.style("fill", function (d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("fill-opacity", ".9")
.style("shape-rendering", "crispEdges")
.style("stroke", function (d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
Similar code had to be modified for accurate positioning text beside rectangles.
I believe the final result looks natural, even though it lost some of the qualities of the original Sankey (like wider connections).

Categories