This simple interaction demonstrates how to filter nodes (and their respective links) in a Force-Directed Graph using D3.js version 4. It follows the new General Update Pattern in order to add and remove elements as needed.
Last active
September 26, 2024 20:39
-
-
Save colbenkharrl/dcb5590173931bb594e195020aaa959d to your computer and use it in GitHub Desktop.
Filtering Nodes on Force-Directed Graphs (D3 V4)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"nodes": [ | |
{ "id": "0", "group": "1" }, | |
{ "id": "1", "group": "2" }, | |
{ "id": "2", "group": "2" }, | |
{ "id": "3", "group": "2" }, | |
{ "id": "4", "group": "2" }, | |
{ "id": "5", "group": "3" }, | |
{ "id": "6", "group": "3" }, | |
{ "id": "7", "group": "3" }, | |
{ "id": "8", "group": "3" } | |
], | |
"links1": [ | |
{ "source": "0", "target": "1", "id": "0"}, | |
{ "source": "0", "target": "2", "id": "1"}, | |
{ "source": "0", "target": "3", "id": "2"}, | |
{ "source": "0", "target": "4", "id": "3"}, | |
{ "source": "1", "target": "5", "id": "4"}, | |
{ "source": "2", "target": "6", "id": "5"}, | |
{ "source": "3", "target": "7", "id": "6"}, | |
{ "source": "4", "target": "8", "id": "7"}, | |
{ "source": "1", "target": "8", "id": "8"}, | |
{ "source": "2", "target": "5", "id": "9"}, | |
{ "source": "3", "target": "6", "id": "10"}, | |
{ "source": "4", "target": "7", "id": "11"} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Click to view more! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<meta charset="UTF-8"> | |
<style> | |
/* style definitions */ | |
button { | |
position: absolute; | |
} | |
#blue { | |
top: 1em; | |
left: 1em; | |
} | |
#orange { | |
top: 1em; | |
left: 8em; | |
} | |
#green { | |
top: 1em; | |
left: 16em; | |
} | |
.node { | |
stroke: white; | |
stroke-width: 2px; | |
} | |
.link { | |
stroke: gray; | |
stroke-width: 4px; | |
} | |
</style> | |
<button type="button" class="filter-btn" id="blue" value="1">Filter Blue</button> | |
<button type="button" class="filter-btn" id="orange" value="2">Filter Orange</button> | |
<button type="button" class="filter-btn" id="green" value="3">Filter Green</button> | |
<svg width="960" height="500"></svg> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script | |
src="https://code.jquery.com/jquery-3.2.1.min.js" | |
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" | |
crossorigin="anonymous"></script> | |
<script> | |
// data stores | |
var graph, store; | |
// svg selection and sizing | |
var svg = d3.select("svg"), | |
width = +svg.attr("width"), | |
height = +svg.attr("height"), | |
radius = 10; | |
// d3 color scales | |
var color = d3.scaleOrdinal(d3.schemeCategory10); | |
var link = svg.append("g").selectAll(".link"), | |
node = svg.append("g").selectAll(".node"); | |
// force simulation initialization | |
var simulation = d3.forceSimulation() | |
.force("link", d3.forceLink() | |
.id(function(d) { return d.id; })) | |
.force("charge", d3.forceManyBody() | |
.strength(function(d) { return -500;})) | |
.force("center", d3.forceCenter(width / 2, height / 2)); | |
// filtered types | |
typeFilterList = []; | |
// filter button event handlers | |
$(".filter-btn").on("click", function() { | |
var id = $(this).attr("value"); | |
if (typeFilterList.includes(id)) { | |
typeFilterList.splice(typeFilterList.indexOf(id), 1) | |
} else { | |
typeFilterList.push(id); | |
} | |
filter(); | |
update(); | |
}); | |
// data read and store | |
d3.json("blocks-data.json", function(err, g) { | |
if (err) throw err; | |
var nodeByID = {}; | |
g.nodes.forEach(function(n) { | |
nodeByID[n.id] = n; | |
}); | |
g.links1.forEach(function(l) { | |
l.sourceGroup = nodeByID[l.source].group.toString(); | |
l.targetGroup = nodeByID[l.target].group.toString(); | |
}); | |
graph = g; | |
store = $.extend(true, {}, g); | |
update(); | |
}); | |
// general update pattern for updating the graph | |
function update() { | |
// UPDATE | |
node = node.data(graph.nodes, function(d) { return d.id;}); | |
// EXIT | |
node.exit().remove(); | |
// ENTER | |
var newNode = node.enter().append("circle") | |
.attr("class", "node") | |
.attr("r", radius) | |
.attr("fill", function(d) {return color(d.group);}) | |
.call(d3.drag() | |
.on("start", dragstarted) | |
.on("drag", dragged) | |
.on("end", dragended) | |
) | |
newNode.append("title") | |
.text(function(d) { return "group: " + d.group + "\n" + "id: " + d.id; }); | |
// ENTER + UPDATE | |
node = node.merge(newNode); | |
// UPDATE | |
link = link.data(graph.links1, function(d) { return d.id;}); | |
// EXIT | |
link.exit().remove(); | |
// ENTER | |
newLink = link.enter().append("line") | |
.attr("class", "link"); | |
newLink.append("title") | |
.text(function(d) { return "source: " + d.source + "\n" + "target: " + d.target; }); | |
// ENTER + UPDATE | |
link = link.merge(newLink); | |
// update simulation nodes, links, and alpha | |
simulation | |
.nodes(graph.nodes) | |
.on("tick", ticked); | |
simulation.force("link") | |
.links(graph.links1); | |
simulation.alpha(1).alphaTarget(0).restart(); | |
} | |
// drag event handlers | |
function dragstarted(d) { | |
if (!d3.event.active) simulation.alphaTarget(0.3).restart(); | |
d.fx = d.x; | |
d.fy = d.y; | |
} | |
function dragged(d) { | |
d.fx = d3.event.x; | |
d.fy = d3.event.y; | |
} | |
function dragended(d) { | |
if (!d3.event.active) simulation.alphaTarget(0); | |
d.fx = null; | |
d.fy = null; | |
} | |
// tick event handler with bounded box | |
function ticked() { | |
node | |
.attr("cx", function(d) { return d.x = Math.max(radius, Math.min(width - radius, d.x)); }) | |
.attr("cy", function(d) { return d.y = Math.max(radius, Math.min(height - radius, d.y)); }); | |
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; }); | |
} | |
// filter function | |
function filter() { | |
// add and remove nodes from data based on type filters | |
store.nodes.forEach(function(n) { | |
if (!typeFilterList.includes(n.group) && n.filtered) { | |
n.filtered = false; | |
graph.nodes.push($.extend(true, {}, n)); | |
} else if (typeFilterList.includes(n.group) && !n.filtered) { | |
n.filtered = true; | |
graph.nodes.forEach(function(d, i) { | |
if (n.id === d.id) { | |
graph.nodes.splice(i, 1); | |
} | |
}); | |
} | |
}); | |
// add and remove links from data based on availability of nodes | |
store.links1.forEach(function(l) { | |
if (!(typeFilterList.includes(l.sourceGroup) || typeFilterList.includes(l.targetGroup)) && l.filtered) { | |
l.filtered = false; | |
graph.links1.push($.extend(true, {}, l)); | |
} else if ((typeFilterList.includes(l.sourceGroup) || typeFilterList.includes(l.targetGroup)) && !l.filtered) { | |
l.filtered = true; | |
graph.links1.forEach(function(d, i) { | |
if (l.id === d.id) { | |
graph.links1.splice(i, 1); | |
} | |
}); | |
} | |
}); | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment