I'm starting a graph project with a single node. When I'm dragging it somewhere, I expect it to smoothly go back to the center of the svg but instead of that, it glitches back to the center.
var simulation = d3.forceSimulation()
.force("center", d3.forceCenter(svgWidth/2, svgHeight/2));
Codepen full example
I think I might not be configuring forceCenter() properly or maybe since there is no other node, there is no other body to interact with so this is how D3 would work.
...maybe since there is no other node, there is no other body to interact with so this is how D3 would work.
Yes, your intuition is (partially) correct here, there is nothing wrong with this force simulation. I reckon that the problem here comes from a misunderstanding of the purpose of forceCenter: forceCenter does not make the node going smoothly to the specified position (more on that below). Actually, if you had another node in that simulation, you'd see that, by dragging one node, the other one would also move, so the center of mass stays at the same place.
That being said, you can see that it works as you expect — that is, the node smoothly going back to the center — if, instead of forceCenter, you used forceX and forceY...
var simulation = d3.forceSimulation()
.force("x", d3.forceX(svgWidth / 2))
.force("y", d3.forceY(svgHeight / 2));
... without changing any alpha, alphaTarget, velocityDecay etc...
Here is the code with that change only:
////////////////////////////////////////////////////
// SVG setup
////////////////////////////////////////////////////
var svgWidth = 400,
svgHeight = 400;
var svgRef = d3.select("body").append("svg")
.attr("width",svgWidth)
.attr("height",svgHeight);
svgRef.append("rect")
.attr("width", svgWidth)
.attr("height", svgHeight);
////////////////////////////////////////////////////
// Data
////////////////////////////////////////////////////
var nodes_data = [{id: "0"}];
var links_data = [];
////////////////////////////////////////////////////
// Links and nodes setup
////////////////////////////////////////////////////
var linksRef = svgRef.selectAll(".link")
.data(links_data).enter()
.append("line")
.attr("class", "link");
var nodesRef = svgRef
.selectAll(".node")
.data(nodes_data).enter()
.append("g").attr("class", "node")
.append("circle")
.attr("r", 10)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
////////////////////////////////////////////////////
// Simulation setup
////////////////////////////////////////////////////
var simulation = d3.forceSimulation()
.force("x", d3.forceX(svgWidth / 2))
.force("y", d3.forceY(svgHeight / 2));
simulation.nodes(nodes_data).on("tick", ticked);
simulation.force("link").links(links_data);
function ticked() {
nodesRef.attr("transform", function (d) {return "translate(" + d.x + "," + d.y + ")";})
linksRef.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; });
}
////////////////////////////////////////////////////
// Dragging
////////////////////////////////////////////////////
function dragstarted (d) {
if (!d3.event.active) simulation.alphaTarget(1).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;
}
.link {
stroke: #000;
stroke-width: 2px;
}
.node {
fill: #CCC;
stroke: #000;
stroke-width: 1px;
}
rect {
fill: none;
stroke: #000;
stroke-width: 2px;
pointer-events: all;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
Related
I have the following implementation of two nodes in a d3 v7 forceSimulation:
function anim() {
d3.select("#test").attr("class", "link anim");
setTimeout(() => {
d3.select("#test").attr("class", "link");
}, 1000);
}
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
graph = {
nodes: [],
links: [],
}
var simulation = d3.forceSimulation()
.force("center", d3.forceCenter(width / 2, height / 2).strength(0.01))
.nodes(graph.nodes)
.force("link", d3.forceLink(graph.links).distance(100))
.on("tick", function() {
svg.selectAll('.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 })
svg.selectAll('.node')
.attr("cx", function (d) { return d.x })
.attr("cy", function (d) { return d.y })
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
})
}).alphaDecay(0.0002)
function update() {
// update links
var link = svg.selectAll('.link').data(graph.links);
link.enter()
.insert('line', '.node')
.attr('class', 'link')
.attr('id', "test")
.style('stroke', '#d9d9d9');
link
.exit()
.remove()
// update nodes
var node = svg.selectAll('.node').data(graph.nodes);
var g = node.enter()
.append('g')
.attr('class', 'node');
g.append('circle')
.attr("r", 20)
.style("fill", "#d9d9d9")
.attr("cursor", "move");
g.append('text')
.attr("class", "text")
.attr("dy", -25)
.text(function (d) { return d.name });
var drag = d3.drag()
.on("drag", function(event, d) {
d.fx = event.x;
d.fy = event.y;
simulation.alpha(0.3).restart();
})
.on("end", function(event, d) {
d.fx = null;
d.fy = null;
});
node.call(drag);
node
.exit()
.remove();
// update simulation
simulation
.nodes(graph.nodes)
.force("link", d3.forceLink(graph.links).distance(100))
.force("charge", d3.forceManyBody().strength(-200))
.restart()
};
function addNode(node) {
graph.nodes.push(node);
update();
};
function connectNodes(source, target) {
graph.links.push({
source: source,
target: target,
});
update();
};
addNode({
id: "you",
name: "you",
});
addNode({
id: "other",
name: "other"
});
connectNodes(0, 1);
.anim {
animation: dash 1s linear forwards;
}
#keyframes dash {
from {
stroke-dasharray: 100;
stroke-dashoffset: 100;
}
to {
stroke-dasharray: 100;
stroke-dashoffset: 0;
}
}
<html>
<head>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<svg width="400" height="200"></svg>
<button onclick="anim()">Animate</button>
</body>
</html>
When the user clicks the "Animate" button, the line between the nodes gets redrawn. This solution is based of an article on how to animate lines being drawn. However, 1) I would like the line to stay static, and have the animation draw a projectile between the nodes instead - like a bullet being fired from "you" to "other". I couldn't figure out how do this with d3.
And 2) is there a better solution than removing the anim class after three seconds inside the anim() function ? I believe one could reset the css animation instead as suggested here: https://css-tricks.com/restart-css-animation/, but I am not sure how this would work in the d3 context. A problem with my solution is that spamming the Animate button does not reset the animation properly. With 1) implemented I would like the spamming effect two draw multiple bullets in succession (it does not need to wait for the animation to finish).
I am using D3js to create a force directed graph.
How can I change the strength of the connections? In my case, I want to reduce the strength of the connections so that the nodes are further apart in general and the overall graph gets a lot looser and more usable. (See image at the end for current result). Changing the value attribute only changed the displayed thickness of the connections, not their "physical" strength.
JSON data (over 12000 lines, so I don't want to put it in this post directly): https://pastebin.com/Ha04adCQ
Code:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<style>
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
text {
font-family: sans-serif;
font-size: 10px;
}
</style>
<svg width="3000" height="1500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<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));
d3.json("pcap_export.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("g")
.data(graph.nodes)
.enter().append("g")
var circles = node.append("circle")
.attr("r", 5)
.attr("fill", function(d) { return color(d.group); });
// Create a drag handler and append it to the node object instead
var drag_handler = d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
drag_handler(node);
var lables = node.append("text")
.text(function(d) {
return d.id;
})
.attr('x', 6)
.attr('y', 3);
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("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
}
});
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(.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;
}
</script>
</head>
<body>
</body>
</html>
Current result:
For any change in property in d3 forceSimulation, you can refer this api https://github.com/d3/d3-force and change the below initialization of the force simulation.
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));
For what I understood you want to increase the distance between links. for that you can use distance property and add it in your initialization of the simulation, something like below. You can try various properties of link, node, center and charge by refering the above api. Add .distance([200]) to your links and see is that what you want.
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}).distance([200]))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
Also if you want no overlapping nodes, add collide property to your force simulation, change the strength between 0 and 1. You will have to read through various properties in the api and add them in initialization till you find desired result
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}).distance([100]))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collide", d3.forceCollide().strength([0.3]));
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'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)
}
}
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>