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.
Related
I am trying to create bubble chart.(I'm new to D3.js).
First, I tried to change the code by referring to this site(https://www.d3-graph-gallery.com/graph/circularpacking_drag.html) to make a bubble chart.
the following code is original one.
// set the dimensions and margins of the graph
var width = 450
var height = 450
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", 450)
.attr("height", 450)
// create dummy data -> just one element per circle
var data = [{
"name": "A"
}, {
"name": "B"
}, {
"name": "C"
}, {
"name": "D"
}, {
"name": "E"
}, {
"name": "F"
}, {
"name": "G"
}, {
"name": "H"
}]
// Initialize the circle: all located at the center of the svg area
var node = svg.append("g")
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("r", 25)
.attr("cx", width / 2)
.attr("cy", height / 2)
.style("fill", "#19d3a2")
.style("fill-opacity", 0.3)
.attr("stroke", "#b3a2c8")
.style("stroke-width", 4)
.call(d3.drag() // call specific function when circle is dragged
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Features of the forces applied to the nodes:
var simulation = d3.forceSimulation()
.force("center", d3.forceCenter().x(width / 2).y(height / 2)) // Attraction to the center of the svg area
.force("charge", d3.forceManyBody().strength(1)) // Nodes are attracted one each other of value is > 0
.force("collide", d3.forceCollide().strength(.1).radius(30).iterations(1)) // Force that avoids circle overlapping
// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
.nodes(data)
.on("tick", function(d) {
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
});
// What happens when a circle is dragged?
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(.03).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(.03);
d.fx = null;
d.fy = null;
}
<div id="my_dataviz"></div>
<script src="https://d3js.org/d3.v5.js"></script>
And in this chart, since there was no attraction function(attract to center),
I tried to add the following code. (I referred to the following site https://blockbuilder.org/ericsoco/d2d49d95d2f75552ac64f0125440b35e)
.force('attract', d3.forceAttract()
.target([width/2, height/2])
.strength(0.01))
However, it is not working.And it changes like the following image.
Could anyone advice me why this happen?
The image you get from d3.forceAttract not existing in d3 v5, as you can see from the console. You can use something like d3.forceRadial, however to add an attraction towards the center:
// set the dimensions and margins of the graph
var width = 450
var height = 450
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", 450)
.attr("height", 450)
// create dummy data -> just one element per circle
var data = [{
"name": "A"
}, {
"name": "B"
}, {
"name": "C"
}, {
"name": "D"
}, {
"name": "E"
}, {
"name": "F"
}, {
"name": "G"
}, {
"name": "H"
}]
// Initialize the circle: all located at the center of the svg area
var node = svg.append("g")
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("r", 25)
.attr("cx", width / 2)
.attr("cy", height / 2)
.style("fill", "#19d3a2")
.style("fill-opacity", 0.3)
.attr("stroke", "#b3a2c8")
.style("stroke-width", 4)
.call(d3.drag() // call specific function when circle is dragged
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Features of the forces applied to the nodes:
var simulation = d3.forceSimulation()
.force("center", d3.forceCenter().x(width / 2).y(height / 2)) // Attraction to the center of the svg area
.force("charge", d3.forceManyBody().strength(1)) // Nodes are attracted one each other of value is > 0
.force("collide", d3.forceCollide().strength(.1).radius(30).iterations(1)) // Force that avoids circle overlapping
.force('attract', d3.forceRadial(0, width / 2, height / 2).strength(0.05))
// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
.nodes(data)
.on("tick", function(d) {
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
});
// What happens when a circle is dragged?
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(.03).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(.03);
d.fx = null;
d.fy = null;
}
<div id="my_dataviz"></div>
<script src="https://d3js.org/d3.v5.js"></script>
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 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>
For d3 force layouts that include drag functionality with d3-drag, it seems that the functions called on each drag event modify d.fx/d.fy, eg:
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
The drag start event often bases d.fx/d.fy on d.x/d.y while the end event sets d.fx/d.fy to null.
Where does d.fx/d.fy come from and why does it get used on elements that are being dragged? Is this built into d3 or d3-force in some way? Where is it assigned to the element being dragged?
d3 force layout and node.fx/fy
Within a d3 force simulation, a node's fx/fy properties can be used to set a fixed position for that node. If the fx/fy values are undefined or null, the nodes is free to move around. If they are set, the x/y properties of the node will always be set to match the fx/fy properties:
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. (docs)
These fx/fy properties are used to fix nodes in general, not just during drag events.
Application to drag events in a d3 force layout:
In a d3 force simulation the position of each node is updated on every tick. The tick fires repeatedly throughout the simulation to keep the nodes position updated, it does so fast enough to appear to animate the nodes movement.
While dragging you want to keep the node's position where the mouse is. During a drag, each time the mouse is moved, the drag event fires. It doesn't fire continuously unless the mouse moves.
When dragging we don't want to apply a force to the node being dragged: we want the node to follow the mouse (we generally also don't want to freeze the rest of the nodes by stopping the simulation during drags).
In order to remove the effects of the force layout on the dragged node, we can set the node.fx/fy properties so that the force doesn't pull the nodes away from the mouse position. When the drag is complete, we want to unset (using null) those values so the force will position the node again.
In the snippet below two force layouts are presented. Each will behave differently:
In the red layout nodes have there fx/fy properties set to the mouse position during the drag.
In the blue layout nodes simply have their x/y properties set to the mouse position during the drag.
In the red layout the force won't re-position a node during a drag. In the blue layout the force will continue to act upon a node during a drag. In the blue example both drag and force continuously place the node based on their individual rules, though normally tick events will generally place the node frequently enough that a drag may not be very visible. Try dragging the blue node a bit then don't move the mouse - it'll drift according to the force layout only:
In both examples the drag functions still update the force layout regarding the position of the dragged node
var data1 ={ "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 data2 ={ "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);
// FIRST SIMULATION
var simulation1 = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(50))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 3, height / 2));
var link1 = svg.append("g")
.selectAll("line")
.data(data1.links)
.enter().append("line")
.attr("stroke","black");
var node1 = svg.append("g")
.selectAll("circle")
.data(data1.nodes)
.enter().append("circle")
.attr("r", 10)
.call(d3.drag()
.on("drag", dragged1)
.on("end", dragended1))
.attr("fill","crimson");
simulation1.nodes(data1.nodes)
.on("tick", ticked1)
.alphaDecay(0)
.force("link")
.links(data1.links);
function ticked1() {
link1
.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; });
node1
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dragged1(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended1(d) {
d.fx = null;
d.fy = null;
}
// SECOND SIMULATION
var simulation2 = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(50))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width * 2 / 3, height / 2));
var link2 = svg.append("g")
.selectAll("line")
.data(data2.links)
.enter().append("line")
.attr("stroke","black");
var node2 = svg.append("g")
.selectAll("circle")
.data(data2.nodes)
.enter().append("circle")
.attr("r", 10)
.call(d3.drag()
.on("drag", dragged2))
.attr("fill","steelblue");
simulation2.nodes(data2.nodes)
.on("tick", ticked2)
.alphaDecay(0)
.force("link")
.links(data2.links);
function ticked2() {
link2
.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; });
node2
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dragged2(d) {
d.x = d3.event.x;
d.y = d3.event.y;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
The d in the drag functions being an individual node in the nodes data array (the node being dragged), from which the force layout bases its calculations and where it updates positions
Also, some drag started events may use d.fx = d.x, this will simply set the node's position to its current position (as I do above), you could also use the mouse's current position without any noticeable difference.
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)
}
}