I've been trying to implement a D3 force directed graph function to give a visual representation to my data, So far I've been successful in getting the nodes to display on the screen and have the individual nodes draggable, however I have been unsuccessful in getting the whole network to pan and zoom.
I've looked at countless examples online, but haven't really been able to figure out what I'm doing wrong.
Could someone point me in the right direction please, using d3 version 4
function selectableForceDirectedGraph(){
var width = d3.select('svg').attr('width');
var height = d3.select('svg').attr('height');
var color = d3.scaleOrdinal(d3.schemeCategory20);
var svg = d3.select("svg")
.attr('width',width)
.attr('height',height);
var container = svg.append("g")
.on("zoom",zoomed)
.on("start",dragstarted)
.on("drag",dragged)
.on("end",dragended);
var json_nodes = _dict['one']['nodes'];
var json_links = _dict['one']['links'];
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d){return d.id}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(json_links)
.enter().append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); })
.style("marker-end","url(#suit)");
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(json_nodes)
.enter().append("circle")
.attr("r", 5)
.attr("fill", function(d) { return color(d.group); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(json_nodes)
.on("tick", ticked);
simulation.force("link")
.links(json_links);
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; });
}
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;
}
//Zoom functions
function zoomed(){
container.attr("transform","translate(" + d3.event.translate + ")scale(" +d3.event.scale + ")");
}
}
The HTML doc that this function is called from has an SVG instantiated (hence why the SVG is selected, not appended like in most other examples)
The two variables "json_nodes" and "json_links" take formatted nodes and links (like what you'd see in a JSON file) from a text file and passes them as the data (wanted to do this offline so when I go overseas). The format for the data is like so:
nodes:[{"id": "Name", "group": integer},...],
links:[{"source": "Name", "target": "Name", "value": integer},...]
I apologise if this is a repeated question, I haven't been able to find any really intuitive help with this.
After some intense staring at the screen for a couple hours, I realised a number of little housekeeping tips to make mine, and hopefully to any other programmers new to D3, applications easier to understand and less prone to errors.
As it turns out from my last example, I was trying to perform zoom functions when the zoom variable containing the D3 .zoom() method hadn't been added to the links variable (as I have pointed out below). Once I had done this, everything worked perfectly.
I've also added some comments as to make the improve the readability of the code from the original question, these changes make it easier to understand and much easier to build upon (much like the inheritance with python classes that I'm familiar with).
So hopefully my little moment of frustration is useful to someone in the future, until the next question, happy debugging :)
Mr Incompetent.
function selectableForceDirectedGraph(){
var width = d3.select('svg').attr('width');
var height = d3.select('svg').attr('height');
var color = d3.scaleOrdinal(d3.schemeCategory20);
//As the height and width have already been set, no need to reset them.
var svg = d3.select("svg");
//This is the container group for the zoom
var container = svg.append("g")
.attr("class","everything");
//see the above question for explanation for purpose of these variable.
var json_nodes = _dict['one']['nodes'];
var json_links = _dict['one']['links'];
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d){return d.id}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
//drawing lines for the links
var link = container.append("g")
.attr("class", "links")
.selectAll("line")
.data(json_links)
.enter().append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); })
.style("marker-end","url(#suit)");
//draw the circles for the nodes
var node = container.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(json_nodes)
.enter().append("circle")
.attr("r", 5)
.attr("fill", function(d) { return color(d.group); });
//HOUSE KEEPING NOTE: add handlers for drag and zoom as to prevent DRY
var drag_controls = d3.drag()
.on("start",dragstarted)
.on("drag",dragged)
.on("end",dragended);
drag_controls(node); //adding the drag event handlers to the nodes
var zoom_controls = d3.zoom()
.on("zoom",zoomed);
zoom_controls(svg); //adding the zoom event handler to the svg container
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(json_nodes)
.on("tick", ticked);
simulation.force("link")
.links(json_links);
function ticked() {
link //updates the link positions
.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 //update the node positions
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
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;
}
//Zoom functions
function zoomed(){
container.attr("transform",d3.event.transform)
}
}
Related
This question already has answers here:
Code within d3.json() callback is not executed
(2 answers)
Closed 4 years ago.
I am not able to figure out what I am doing wrong in the following html code. I have a JSON file with a "cohesive" attribute which is a continuous variable. I want to draw a force directed graph on D3JS using the JSON file with the node color reflecting the difference in cohesive energy. When I run this script, I don't get any errors but there is no output as well. I just started with HTML scripting so please forgive me if I am making a very silly error but I would really appreciate if someone could point out what I am doing wrong.
function convertRange(value, r1, r2) {
return (value - r1[0]) * (r2[1] - r2[0]) / (r1[1] - r1[0]) + r2[0];
}
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var colorScale = d3.scaleLinear(d3.interpolateViridis);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
d3.json("Perovskite_info.json", function(error, graph) {
if (error) throw error;
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("stroke-width", function(d) {
return Math.sqrt(d.value);
});
var node = svg.append("g")
.attr("nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 5)
.attr("fill", function(d) {
return colorScale(convertRange(d.cohesive_en, [1.6, 7.2], [0, 1]));
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
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;
});
}
});
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;
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: black;
stroke-width: 1.5px;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<h1>Cohesive Energy Calculation</h1>
<svg width="1060" height="800"></svg>
This is how one my json begins:
{"directed": false, "multigraph": false, "graph": {}, "nodes": [{"pos": [-37.50340668168362, -0.6442713601030169], "cohesive_en": 5.752120797750001, "cohesive": 5.752120797750001, "cluster": 1, "similarity": 0.6371195466189876, "id": "PrCoO3"},
For the full JSON please follow this link: https://api.myjson.com/bins/m3gd8
Edit: So I figured out what the problems were:
First off, as one of the answers stated, I was using d3.v5 but using the d3v4 api to import data. Secondly, I also needed to import the d3 interpolate module which I hadn't done. So the code wasn't working. This question was asked at a point where I had done d3 for 2 days so the quality of the code is low.
you use a D3v4 API of loading a file.
D3v5 use the Fetch API
// d3.json("Perovskite_info.json", function(error, graph) {
d3.json("Perovskite_info.json").then( function(graph) {
//if (error) throw error;
Then there is a problem in how you add the nodes compared to the links but you will find that.
Why do the colorScale Math yourself in convertRange?
There is no color yet but that is a different problem.
I'm trying to create a force layout with d3.js v4 and the mayor part is working really well accept for one problem when using the drag behavior.
When clicking on a node to drag it around the other nodes, which are not connected to the clicked node, fly away:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
function drawNode(nodes) {
return svg.selectAll("circle.node")
.data(nodes, function (d, i) { return d.Id; })
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.attr("fill", "blue")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
}
function drawMainNode(nodes) {
return svg.selectAll("circle.mainnode")
.data(nodes, function (d, i) { return d.Id; })
.enter().append("circle")
.attr("class", "mainnode")
.attr("r", 15)
.attr("fill", "red")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
}
function drawLinks(links) {
return svg.selectAll("line")
.data(links, function (d, i) { return d.source + "-" + d.target; })
.enter().append("line")
.attr("stroke-width", 3);
}
function dragstarted(d) {
if (!d3.event.active) dragSimualtion.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) dragSimualtion.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function ticked() {
dLinks
.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; });
dNodes
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
dMainNodes
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
var dLinks = drawLinks(mainLinks.concat(nodeLinks));
var dNodes = drawNode(nodes);
var dMainNodes = drawMainNode(mainNodes);
var simulation = d3.forceSimulation(mainNodes.concat(nodes))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collide", d3.forceCollide(20).radius(20))
.force("links", d3.forceLink(mainLinks.concat(nodeLinks))
.id(function (d) { return d.Id; }))
.alpha(0.9)
.alphaDecay(0.1)
.on("tick", ticked);
var dragSimualtion = d3.forceSimulation(mainNodes.concat(nodes))
.force("collide", d3.forceCollide(20).radius(20))
.force("links", d3.forceLink(mainLinks.concat(nodeLinks))
.id(function (d) { return d.Id; }))
.alphaDecay(0.1)
.on("tick", ticked)
.stop()
Plunkr
I tried using a seperate simulation for the drag event which only uses the link force and collision force - because the nodes should be dragged around with every node attached to them and other non-attached nodes should be pushed away, but only if they collide.
So the question is, how can i prevent the other nodes from flying around?
Edit1: My first attempt was using only the first simulation object, but i thought the observed effect was caused by the d3.forceManyBody due to its repulsion force. I tried to limit this effect with distanceMax but had no success. My second attempt was to use a seperate d3.forceSimulation which does only contain the forces relevant for the drag animation.
the forceCollide of the drag function appears to be the problem...
try:
.force("collide", d3.forceCollide(20).radius(20).strength(0))
then you get the opposite problem, then try between 0 and 1, find one you like.
The attached snippet contains a modified version of the official d3 force graph example code. When I insert the graph immediately everything works as expected. If however, I insert the graph dynamically (which you can do by pressing Clear and Redraw in the demo) the nodes do not spread out the same way. Sometimes they even stay in the top-left corner of the svg.
One hack I found was to add simulation.alphaTarget(1).restart() after inserting the graph. This unfortunately takes longer to reach a stable output and can lead to residual tremors (or spinning).
How do make the dynamically inserted graph have the behavior of the graph inserted immediately on page load without my hack?
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var color = d3.scaleOrdinal(d3.schemeCategory20);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
// I wrapped the d3.json invocation in this function
function drawGraph() {
d3.json("https://gist.githubusercontent.com/mbostock/4062045/raw/5916d145c8c048a6e3086915a6be464467391c62/miserables.json", function(error, graph) {
if (error) throw error;
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.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(graph.nodes)
.enter().append("circle")
.attr("r", 5)
.attr("fill", function(d) { return color(d.group); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
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; });
}
});
}
// When I call it hear, all is well (as you would expect).
drawGraph()
// But when I clear and redraw it (by pressing a button), the nodes
// don't spread out.
function clearRedraw() {
d3.selectAll("svg > *").remove()
drawGraph()
}
function hackySolution() {
d3.selectAll("svg > *").remove()
drawGraph()
simulation.alphaTarget(1).restart()
}
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;
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<button onclick="clearRedraw()">Clear And Redraw</button>
<button onclick="hackySolution()">Hacky Solution</button>
<svg width="960" height="600"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>
</body>
</html>
That's not a hacky solution! That's the correct, idiomatic way to re-heat the simulation.
The problem here is that when you do...
d3.selectAll("svg > *").remove()
... you are only removing the DOM elements. The simulation, however, is still running, and has cooled down.
Actually, if you wait until the simulation is completely finished (some 5 seconds) before clicking "Clear and Redraw", you're gonna see that the nodes always pile up at the origin (top left corner). Try to click on them: they will move to the center (because the drag function kind of re-heat the simulation, since it has an alphaTarget).
Therefore, you have to re-heat it.
However, instead of using alphaTarget, you should use alpha:
simulation.alpha(0.8).restart()
Here is the code with that change:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var color = d3.scaleOrdinal(d3.schemeCategory20);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
// I wrapped the d3.json invocation in this function
function drawGraph() {
d3.json("https://gist.githubusercontent.com/mbostock/4062045/raw/5916d145c8c048a6e3086915a6be464467391c62/miserables.json", function(error, graph) {
if (error) throw error;
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.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(graph.nodes)
.enter().append("circle")
.attr("r", 5)
.attr("fill", function(d) { return color(d.group); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
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; });
}
});
}
// When I call it hear, all is well (as you would expect).
drawGraph()
// But when I clear and redraw it (by pressing a button), the nodes
// don't spread out.
function clearRedraw() {
d3.selectAll("svg > *").remove()
drawGraph()
}
function hackySolution() {
d3.selectAll("svg > *").remove()
drawGraph()
simulation.alpha(0.8).restart()
}
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;
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<button onclick="clearRedraw()">Clear And Redraw</button>
<button onclick="hackySolution()">Hacky Solution</button>
<svg width="960" height="600"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>
</body>
</html>
Alternatively, if you still feel that re-heating the simulation is a hacky solution (which it is not), just move the simulation assignment to inside the drawGraph function:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var color = d3.scaleOrdinal(d3.schemeCategory20);
// I wrapped the d3.json invocation in this function
function drawGraph() {
d3.json("https://gist.githubusercontent.com/mbostock/4062045/raw/5916d145c8c048a6e3086915a6be464467391c62/miserables.json", function(error, graph) {
if (error) throw error;
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.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(graph.nodes)
.enter().append("circle")
.attr("r", 5)
.attr("fill", function(d) {
return color(d.group);
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(function(d) {
return d.id;
});
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
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;
});
}
});
}
// When I call it hear, all is well (as you would expect).
drawGraph()
// But when I clear and redraw it (by pressing a button), the nodes
// don't spread out.
function clearRedraw() {
d3.selectAll("svg > *").remove()
drawGraph()
}
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;
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
<button onclick="clearRedraw()">Clear And Redraw</button>
<svg width="960" height="600"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>
I am trying to represent my data with d3js force layout. I have the following code:
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var color = d3.scaleOrdinal(d3.schemeCategory20);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var jsonData = JSON.parse(data);
var totalAmount=jsonData.total;
d3.json("myData.json", function(error, graph) {
if (error) throw error;
var nodes = json.nodes;
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
//.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
.attr("stroke-width",4)
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 5)
.attr("fill", function(d) { return color(d.group); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
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; });
}
});
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;
}
However, in the output, the nodes are so close to each others like in this photo:
My result
I checked that links are correct (basically I have one source and so many target nodes.)
How can I make larger the distance between nodes? Thanks for any help.
Try this
simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(value).id(function(d) { return d.id; }))
...
Where value is some number
I want some of the nodes in my force-directed layout to ignore all forces and stay in fixed positions based on an attribute of the node, while still being able to be dragged and exert repulsion on other nodes and maintain their link lines.
I thought it would be as simple as this:
force.on("tick", function() {
vis.selectAll("g.node")
.attr("transform", function(d) {
return (d.someAttribute == true) ?
"translate(" + d.xcoordFromAttribute + "," + d.ycoordFromAttribute +")" :
"translate(" + d.x + "," + d.y + ")"
});
});
I have also tried to manually set the node's x and y attributes each tick, but then the links continue to float out to where the node would be if it was affected by the force.
Obviously I have a basic misunderstanding of how this is supposed to work. How can I fix nodes in a position, while keeping links and still allowing for them to be draggable?
Set d.fixed on the desired nodes to true, and initialize d.x and d.y to the desired position. These nodes will then still be part of the simulation, and you can use the normal display code (e.g., setting a transform attribute); however, because they are marked as fixed, they can only be moved by dragging and not by the simulation.
See the force layout documentation for more details (v3 docs, current docs), and also see how the root node is positioned in this example.
Fixed nodes in force layout for d3v4 and d4v5
In d3v3 d.fixed will fix nodes at d.x and d.y; however, in d3v4/5 this method no longer is supported. The d3 documentation states:
To fix a node in a given position, you may specify two additional
properties:
fx - the node’s fixed x-position
fy - the node’s fixed y-position
At the end of each tick, after the application of any forces, a node
with a defined node.fx has node.x reset to this value and node.vx set
to zero; likewise, a node with a defined node.fy has node.y reset to
this value and node.vy set to zero. To unfix a node that was
previously fixed, set node.fx and node.fy to null, or delete these
properties.
You can set fx and fy attributes for the force nodes in your data source, or you can add and remove fx and fy values dynamically. The snippet below sets these properties at the end of drag events, just drag a node to fix its position:
var data ={
"nodes":
[{"id": "A"},{"id": "B"},{"id": "C"},{"id":"D"}],
"links":
[{"source": "A", "target": "B"},
{"source": "B", "target": "C"},
{"source": "C", "target": "A"},
{"source": "D", "target": "A"}]
}
var height = 250;
var width = 400;
var svg = d3.select("body").append("svg")
.attr("width",width)
.attr("height",height);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(50))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var link = svg.append("g")
.selectAll("line")
.data(data.links)
.enter().append("line")
.attr("stroke","black");
var node = svg.append("g")
.selectAll("circle")
.data(data.nodes)
.enter().append("circle")
.attr("r", 5)
.call(d3.drag()
.on("drag", dragged)
.on("end", dragended));
simulation
.nodes(data.nodes)
.on("tick", ticked)
.alphaDecay(0);
simulation.force("link")
.links(data.links);
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; });
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.6.0/d3.min.js"></script>
d3v6 changes to event listners
In the above snippet, the drag events use the form
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
Where d is the datum of the node being dragged. In d3v6, the form is now:
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
or:
function dragged(event,d) {
d.fx = event.x;
d.fy = event.y;
}
The event is now passed directly to the listener, the second parameter passed to the event listener is the datum. Here's the canonical example on Observable.