A modified version of the original "force layout" created by Mike Bostock, with highlighting features an also labels that appear on mouseover.
Built with blockbuilder.org
xxxxxxxxxx
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.12.0/d3.min.js"></script>
<style>
svg {
position: relative;
display: block;
float: left;
clear: both;
}
.links line {
stroke: darkgrey;
stroke-opacity: .9;
}
.nodes circle {
stroke: black;
stroke-width: 1px;
}
button {
position: relative;
display: block;
float: left;
margin-top: 20px;
margin-left: 200px;
}
</style>
<body>
</body>
<script>
// Define SVG, colorScale, height, width, and initialize simulation.
var width = 750;
var height = 500;
var svg = d3.select("body").append("svg").attr("width",width).attr("height",height)
var colorScale = d3.scaleOrdinal(d3.schemeCategory20);
// Ingest our data.
d3.json("miserables.json", function(error, data){
if(error) throw error;
// Define a simulation, attach the forces, and finally attach the nodes. Add an "ontick" event
// listener to the nodes. This will fire the update, 'ticked' function.
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.name; }))
// The "id" method returns the "name" of each node, rather than the id. This is important, because our links are not indexed based, but have the source and target of the relevant nodes. If our links data were like this: [{"source": 0, "target": 1},{"source": 0, "target": 2}] then using the id method would not be needed, so long as our nodes were kept in the same index order.
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
simulation // We're separating the simulation (d3.forceSimulation) here, to allow us to bind both our nodes to it, and to later select specific forces (as we do below.)
.nodes(data.nodes)
.on("tick", ticked)
simulation.force("link") // here we're re-selecting the "link" force. We can't include it all in one dot-chained method because then the other forces wouldn't be attached to d3.forceSimulation.
.links(data.links)
// Create our link and nodes. It isn't necessary to assign them initial x and y values, which are assigned in our "ticked" function.
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(data.links)
.enter().append("line")
.attr("stroke-width", function(d){ return Math.sqrt(d.value)})
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(data.nodes)
.enter().append("circle")
.attr("r", 5)
.attr("fill", function(d){ return colorScale(d.group)})
.on("mouseover", connectedNodes)
.on("mouseleave", reset)
node.append("title")
.text(function(d){ return d.id})
function ticked(){
link
.attr("x1", function(d){return d.source.x;})
.attr("x2", function(d){return d.target.x;})
.attr("y1", function(d){return d.source.y;})
.attr("y2", function(d){return d.target.y;})
node
.attr("cx", function(d){return d.x})
.attr("cy", function(d){return d.y})
}
// Create an index of what is connected to what
var toggle = 0;
linkedIndex = {}
// Go through the links, for each, add the source and target indexes to our object.
data.links.forEach(function(d){
linkedIndex[d.source.index+","+d.target.index] = 1;
linkedIndex[d.source.index+","+d.source.index] = 1;
linkedIndex[d.target.index+","+d.target.index] = 1; // Every node is connected to itself.
})
function checkNeighbors(a,b){
// Returns the index with a and b (which will later be nodes)
return linkedIndex[a.index+","+b.index]
}
function connectedNodes(){
d = d3.select(this).node().__data__; // Get the data of the node you're hovering over.
console.log(d)
node.transition().style("opacity", function(y){ // Select all nodes and style them...pass each node into a function
// if either connection exists (true), style it 1, otherwise style the node .1
return checkNeighbors(d,y) | checkNeighbors(y,d) ? 1 : 0.1; //
});
link.transition().style("opacity", function(y){
return d.index == y.source.index | d.index == y.target.index ? 1 : 0.1;
})
// Filter nodes by using checkNeighbors
var filtered = node.filter(function(y){
return checkNeighbors(d,y) | checkNeighbors(y,d) ? true : false;
})._groups[0]
var filteredData = [];
filtered.forEach(function(y){
filteredData.push(y.__data__)
})
var entSelection = svg.selectAll("text").data(filteredData)
entSelection.enter()
.append("text")
.attr("x", d => d.x)
.attr("y", d => d.y + 15)
.style("text-anchor","middle")
.style("font-size","10px")
.text(d => d.name)
.attr("pointer-events", "none")
}
function reset(){
node.transition().style("opacity",1)
link.transition().style("opacity",1)
svg.selectAll("text").remove();
}
})
</script>
https://cdnjs.cloudflare.com/ajax/libs/d3/4.12.0/d3.min.js