简体   繁体   中英

“Hard” filtering of nodes in a d3,js force-directed graph

I have a force-directed graph with a (large) number of nodes and many links between them. I want to interactively apply a filter so that only a subset of the nodes remain (and any links between them). However, because the graph is large I want to remove from the simulation any nodes that are filtered out rather than just hiding them (so that the resulting graph has better performance). I am therefore looking to create a filtered list of nodes as a new array and re-initialise the simulation with only these nodes. Same applies to the edges - I have not done this yet but I would presumably need to programmatically determine which edges to retain and do the same sort of filtering before re-initialising the graph with the new nodes and edges. I want to retain the original nodes/edges arrays to allow "resetting" back to the starting state.

I have put together a simple example which at this point is just doing some hard coded filtering but I am struggling to re-initialise the simulation using the filtered arrays. It seems to be removing the filtered node from the simulation (circle "three" becomes no longer draggable) but it is still shown in the rendered graph.

My attempt at the "reset" logic does so far seem to work though.

What am I doing wrong? Is there a better way to achieve this? (d3.js v3)

My example code as follows:

 var links = [{ source: 0, target: 1, type: "c" }, { source: 1, target: 2, type: "d" }, { source: 2, target: 0, type: "d" } ]; var nodes = [{ name: "one", type: "a" }, { name: "two", type: "a" }, { name: "three", type: "b" } ]; var width = 300; var height = 300; var force = d3.layout.force() .nodes(nodes) .links(links) .size([width, height]) .linkDistance(200) .charge(-400) .on("tick", tick) .start(); var svg = d3.select("#graph").append("svg") .attr("width", width) .attr("height", height); function colours(n) { var colours = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac" ]; return colours[n % colours.length]; } var path = svg.append("g").selectAll("path") .data(force.links()) .enter().append("line") .attr('class', 'link') .attr('stroke', function(d, i) { return colours(i); }) var circles = svg.append("g"); var circle = circles.selectAll("circle") .data(force.nodes()) .enter().append("circle") .attr("r", 8) .attr('class', 'circle') .attr('fill', function(d, i) { return colours(i + 3); }) .call(force.drag); var text = svg.append("g").selectAll("text") .data(force.nodes()) .enter().append("text") .attr("x", 14) .attr("y", ".31em") .text(function(d) { return d.name; }); function tick() { path.attr({ x1: function(d) { return d.source.x; }, y1: function(d) { return d.source.y; }, x2: function(d) { return d.target.x; }, y2: function(d) { return d.target.y; } }); circle.attr("transform", transform); text.attr("transform", transform); } function transform(d) { return "translate(" + dx + "," + dy + ")"; } var nodeText = ""; function nodeTypeA(node) { return (node.type == "a"); } function linkTypeC(link) { return (link.type == "c"); } function applyFilter() { force.nodes(nodes.filter(nodeTypeA)); force.links(links.filter(linkTypeC)); circle.data(force.nodes()); text.data(force.nodes()); path.data(force.links()); d3.selectAll("circle").each( function(d) { console.log(d.name); } ); console.log(""); } function resetFilter() { force.nodes(nodes); force.links(links); circle.data(force.nodes()); text.data(force.nodes()); path.data(force.links()); d3.selectAll("circle").each( function(d) { console.log(d.name); } ) console.log(""); } 
 #buttons { position: absolute; top: 10px; left: 20px; height: 100px; width: 400px; z-index: 99; } #graph { position: absolute; top: 50px; left: 20px; height: 300px; width: 300px; z-index: 98; } 
 <script src="https://d3js.org/d3.v3.min.js"></script> <!DOCTYPE html> <meta charset="utf-8"> <body> <div id="root"> <div id="buttons"> <button id="filter" onclick="applyFilter()">Apply</button> <button id="reset" onclick="resetFilter()">Reset</button> </div> <div id="graph"> </div> </div> </body> <div id="node_details"> </div> </body> 

For your filter and reset functions you update the data of the selection but do not use an enter or exit selection to add/remove new elements.

You do use an enter selection when initially appending elements, eg:

var path = svg.append("g").selectAll("path")
 .data(force.links())
 .enter().append("line")
 ...

But when updating you simply use:

path.data(force.links());

You still need to specify what you are adding and how you want to add it, as you did when originally adding the nodes.

To exit nodes is pretty simple, we just add .exit().remove() to the above line when applying the filter. .exit() selects the elements in the selection that no longer have a corresponding item in the data array. .remove() just removes them from the DOM:

 var links = [ {source: 0, target: 1, type: "c"}, {source: 1, target: 2, type: "d"}, {source: 2, target: 0, type: "d"} ]; var nodes = [ {name: "one", type: "a"}, {name: "two", type: "a"}, {name: "three", type: "b"} ]; var width = 300; var height = 300; var force = d3.layout.force() .nodes(nodes) .links(links) .size([width, height]) .linkDistance(200) .charge(-400) .on("tick", tick) .start(); var svg = d3.select("#graph").append("svg") .attr("width", width) .attr("height", height); function colours(n) { var colours = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"]; return colours[n % colours.length]; } var path = svg.append("g").selectAll("path") .data(force.links()) .enter().append("line") .attr('class', 'link') .attr('stroke', function(d, i) { return colours(i); }) var circles = svg.append("g"); var circle = circles.selectAll("circle") .data(force.nodes()) .enter().append("circle") .attr("r", 8) .attr('class', 'circle') .attr('fill', function(d, i) { return colours(i + 3); }) .call(force.drag); var text = svg.append("g").selectAll("text") .data(force.nodes()) .enter().append("text") .attr("x", 14) .attr("y", ".31em") .text(function(d) { return d.name; }); function tick() { path.attr({ x1: function(d) { return d.source.x; }, y1: function(d) { return d.source.y; }, x2: function(d) { return d.target.x; }, y2: function(d) { return d.target.y; } }); circle.attr("transform", transform); text.attr("transform", transform); } function transform(d) { return "translate(" + dx + "," + dy + ")"; } var nodeText = ""; function nodeTypeA(node) { return (node.type == "a"); } function linkTypeC(link) { return (link.type == "c"); } function applyFilter() { force.nodes(nodes.filter(nodeTypeA)); force.links(links.filter(linkTypeC)); circle.data(force.nodes()).exit().remove(); text.data(force.nodes()).exit().remove(); path.data(force.links()).exit().remove(); d3.selectAll("circle").each( function(d) { console.log(d.name); } ); console.log(""); } function resetFilter() { force.nodes(nodes); force.links(links); circle.data(force.nodes()); text.data(force.nodes()); path.data(force.links()); d3.selectAll("circle").each( function(d) { console.log(d.name); } ) console.log(""); } 
 #buttons { position: absolute; top: 10px; left: 20px; height: 100px; width: 400px; z-index: 99; } #graph { position: absolute; top: 50px; left: 20px; height: 300px; width: 300px; z-index: 98; } 
 <div id="root"> <div id="buttons"> <button id="filter" onclick="applyFilter()">Apply</button> <button id="reset" onclick="resetFilter()">Reset</button> </div> <div id="graph"> </div> </div> </body> <script src="https://d3js.org/d3.v3.min.js"></script> 

We could replicate the code you use for the initial enter in the reset function to enter elements (along with some minor modifications) but this is a bit repetitive - we would have two sections of code that do the same thing.

Instead let's put the entering and exiting into an update function. The update function will take the nodes and links from the force layout and enter/exit as needed:

 var links = [ {source: 0, target: 1, type: "c"}, {source: 1, target: 2, type: "d"}, {source: 2, target: 0, type: "d"} ]; var nodes = [ {name: "one", type: "a"}, {name: "two", type: "a"}, {name: "three", type: "b"} ]; var width = 300; var height = 300; var force = d3.layout.force() .nodes(nodes) .links(links) .size([width, height]) .linkDistance(200) .charge(-400) .on("tick", tick) .start(); var svg = d3.select("#graph").append("svg") .attr("width", width) .attr("height", height); function colours(n) { var colours = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"]; return colours[n % colours.length]; } var paths = svg.append("g"); var circles = svg.append("g"); var texts = svg.append("g"); update(); function tick() { paths.selectAll("line").attr({ x1: function(d) { return d.source.x; }, y1: function(d) { return d.source.y; }, x2: function(d) { return d.target.x; }, y2: function(d) { return d.target.y; } }); circles.selectAll("circle").attr("transform", transform); texts.selectAll("text").attr("transform", transform); } function transform(d) { return "translate(" + dx + "," + dy + ")"; } var nodeText = ""; function nodeTypeA(node) { return (node.type == "a"); } function linkTypeC(link) { return (link.type == "c"); } function applyFilter() { force.nodes(nodes.filter(nodeTypeA)); force.links(links.filter(linkTypeC)); update(); } function resetFilter() { force.nodes(nodes); force.links(links); update(); force.start(); // start the force layout again. } function update() { // update the data for the lines: var path = paths.selectAll("line") .data(force.links()); // enter new lines: path.enter().append("line") .attr('class', 'link') .attr('stroke', function(d, i) { return colours(i); }) // exit unneeded lines: path.exit().transition().style("opacity",0).remove(); // update the data for the circles: var circle = circles.selectAll("circle") .data(force.nodes()); // enter new circles: circle.enter().append("circle") .attr("r", 8) .attr('class', 'circle') .attr('fill', function(d, i) { return colours(i + 3); }) .call(force.drag); // remove unneeded circles: circle.exit().transition().style("opacity",0).remove(); // update the text data: var text = texts.selectAll("text") .data(force.nodes()); // enter new text text.enter().append("text") .attr("x", 14) .attr("y", ".31em") .text(function(d) { return d.name; }); // exit old text: text.exit().transition().style("opacity",0).remove(); } 
 #buttons { position: absolute; top: 10px; left: 20px; height: 100px; width: 400px; z-index: 99; } #graph { position: absolute; top: 50px; left: 20px; height: 300px; width: 300px; z-index: 98; } 
 <script src="https://d3js.org/d3.v3.min.js"></script> <div id="root"> <div id="buttons"> <button id="filter" onclick="applyFilter()">Apply</button> <button id="reset" onclick="resetFilter()">Reset</button> </div> <div id="graph"> </div> </div> </body> <div id="node_details"> </div> 

Changes from original: The filter and reset functions call the update function after setting the force's nodes and links (and the nodes are initially drawn with the update function). The force is reset when adding nodes (to re-energize the simulation as if it has cooled, the tick won't be called and nodes won't be positioned properly).

Lastly, text, circles, and lines are in parent g selections named texts circles , and lines respectively. The tick function has been modified to reselect the children of each parent g each tick - though you could optimize this differently.

As a last note, it might be worth looking at the specifying an identifier for the data or specifying node/link properties in the data - when removing/adding links/nodes coloring and/or setting properties by index may be problematic.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM