I am trying to make a node graph in D3 with hovering effects.
example graphics
or something like that:
D3 – force-directed-graph-component
Currently I am using: D3V4.
https://cdn.jsdelivr.net/npm/d3@4
I’d like to have a effect on connected lines and nodes, while hovering a node.
But suddenly it wont work and the debugger tell me there is no known variable defined.
For easy debugging I made the stroke-width very high.
var svg = d3.select("svg"),
width = +svg.node().getBoundingClientRect().width,
height = +svg.node().getBoundingClientRect().height;
// svg objects
var link, node;
// the data - an object with nodes and links
var graph;
// load the data
d3.json("data.json", function(error, _graph) {
if (error) throw error;
graph = _graph;
initializeDisplay();
initializeSimulation();
});
let div = d3.select("body").append("div")
.attr("class", "tooltip-circles")
.style("opacity", 0);
let mouseOver = function(d) {
d3.selectAll(".circle")
.transition()
.duration(200)
.style("opacity", .5);
d3.select(this)
.transition()
.duration(200)
.style("opacity", 1);
div.transition()
.duration(200)
.style("opacity", 1);
div.html(d.id)
.style("left", (d3.event.pageX + 20) + "px")
.style("top", (d3.event.pageY - 25) + "px");
d3.selectAll(".links").filter(function(g) {
console.log(d.id)
console.log(g)
return d.id === g.source || d.id === g.target;
})
.style("stroke-width",444)
};
let mouseLeave = function(d) {
d3.selectAll(".circle")
.transition()
.duration(200)
.style("opacity", 1)
d3.select(this)
.transition()
.duration(200)
div.transition()
.duration('50')
.style("opacity", 0)
d3.selectAll(".links")
.transition()
.duration('200')
.style("opacity", 1)
.style("stroke", "000");
}
//////////// FORCE SIMULATION ////////////
// force simulator
var simulation = d3.forceSimulation();
// set up the simulation and event to update locations after each tick
function initializeSimulation() {
simulation.nodes(graph.nodes);
initializeForces();
simulation.on("tick", ticked);
}
// values for all forces
forceProperties = {
center: {
x: 0.5,
y: 0.5
},
charge: {
enabled: true,
strength: -30,
distanceMin: 1,
distanceMax: 2000
},
collide: {
enabled: true,
strength: .7,
iterations: 1,
radius: 5
},
forceX: {
enabled: false,
strength: .1,
x: .5
},
forceY: {
enabled: false,
strength: .1,
y: .5
},
link: {
enabled: true,
distance: 30,
iterations: 1
}
}
// add forces to the simulation
function initializeForces() {
// add forces and associate each with a name
simulation
.force("link", d3.forceLink())
.force("charge", d3.forceManyBody())
.force("collide", d3.forceCollide())
.force("center", d3.forceCenter())
.force("forceX", d3.forceX())
.force("forceY", d3.forceY());
// apply properties to each of the forces
updateForces();
}
// apply new force properties
function updateForces() {
// get each force by name and update the properties
simulation.force("center")
.x(width * forceProperties.center.x)
.y(height * forceProperties.center.y);
simulation.force("charge")
.strength(forceProperties.charge.strength * forceProperties.charge.enabled)
.distanceMin(forceProperties.charge.distanceMin)
.distanceMax(forceProperties.charge.distanceMax);
simulation.force("collide")
.strength(forceProperties.collide.strength * forceProperties.collide.enabled)
.radius(forceProperties.collide.radius)
.iterations(forceProperties.collide.iterations);
simulation.force("forceX")
.strength(forceProperties.forceX.strength * forceProperties.forceX.enabled)
.x(width * forceProperties.forceX.x);
simulation.force("forceY")
.strength(forceProperties.forceY.strength * forceProperties.forceY.enabled)
.y(height * forceProperties.forceY.y);
simulation.force("link")
.id(function(d) {return d.id;})
.distance(forceProperties.link.distance)
.iterations(forceProperties.link.iterations)
.links(forceProperties.link.enabled ? graph.links : []);
// updates ignored until this is run
// restarts the simulation (important if simulation has already slowed down)
simulation.alpha(1).restart();
}
//////////// DISPLAY ////////////
// generate the svg objects and force simulation
function initializeDisplay() {
// set the data and properties of link lines
link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("class", "links" )
.attr("target", (d) => d.target )
.attr("source", (d) => d.source )
.style("stroke-width", (d) => d.value);
// set the data and properties of node circles
node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.style("stroke", (d) => d.color)
.style("stroke-width", (d) => d.width)
.style('fill', (d) => d.color)
.attr("class", function(d){ return "circle" } )
.on("mouseover", mouseOver )
.on("mouseleave", mouseLeave )
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
updateDisplay();
}
// update the display based on the forces (but not positions)
function updateDisplay() {
node
.attr("r", forceProperties.collide.radius)
.attr("stroke", forceProperties.charge.strength > 0 ? "blue" : "red")
.attr("stroke-width", forceProperties.charge.enabled==false ? 0 : Math.abs(forceProperties.charge.strength)/15);
link
.attr("stroke-width", forceProperties.link.enabled ? 1 : .5)
.attr("opacity", forceProperties.link.enabled ? 1 : 0);
}
// update the display positions after each simulation tick
function ticked() {
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; });
d3.select('#alpha_value').style('flex-basis', (simulation.alpha()*100) + '%');
}
//////////// UI EVENTS ////////////
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.0001);
d.fx = null;
d.fy = null;
}
// update size-related forces
d3.select(window).on("resize", function(){
width = +svg.node().getBoundingClientRect().width;
height = +svg.node().getBoundingClientRect().height;
updateForces();
});
// convenience function to update everything (run after UI input)
function updateAll() {
updateForces();
updateDisplay();
};
I Compared many other solutions, but every other is calling all line, and this breaks the hover-end effect.
Something like:
link.style("stroke", "");", function(o) {
return o.source === d || o.target === d
? '000' : 'AAA';
});
Is not helping and I would have to make every style attribute separate.
I’d like to have a selectAll with a filter for better readability.