How to convert this d3 v3 graph to a v4? - d3.js

I have been trying to convert this example in d3 v3 to a v4 version but still having trouble.
http://bl.ocks.org/eesur/be2abfb3155a38be4de4
Here is a JSFiddle of what I have so far: https://jsfiddle.net/echilee/sfbcntph/135/
I am able to console.log the LINKS and the NODES. I made some progress and am able to see the nodes and click to open and close those that are connected. But cannot see links and how they should connect the nodes.
I replaced the nodes and links declaration and force function with this code:
// create a hierarchy from the root
const treeRoot = hierarchy(root)
tree(treeRoot)
const nodes = flatten(treeRoot.descendants())
const links = treeRoot.links()
var simulation = d3.forceSimulation()
I am not able to see the links.

Related

Tracking d3.zoom transform across page refresh

I'm having a problem with tracking Transforms across page loads, any help much appreciated.
'workspaceDiv' is a full page outer div
'squaregroup' is a g that contains all page elements and can be moved around
For this example I've added a single circle to the squaregroup
workspaceDiv = d3.select("#workspaceDiv")
squaregroup = workspaceDiv.append("g")
.attr("id", "squaregroup")
squaregroup.append("circle").attr("cx", 20).attr("cy", 20).attr("r", 10);
To allow the user to move the g around the page I've attached a d3.zoom.
workspaceDiv.call(zoom);
var zoom = d3.zoom()
.on("zoom", zoomed)
function zoomed(){
squaregroup.attr("transform", d3.event.transform)
}
You might have noticed that I want to transform squaregroup but I have attached the d3.zoom to the workspaceDiv. This is so you can transform it by clicking anywhere on the page (and not only by clicking in the small squaregroup).
On initial page load, this works perfect. Any transforms are also saved as a string in the URL successfully.
On a page reload, the transform is taken from the URL and applied to the sqauregroup:
squaregroup.attr("transform", d3.zoomIdentity.translate(url.x,url.y).scale(url.scale))
Chrome devtools showing the custom transform applied after page reload
[2]: https://i.stack.imgur.com/H93Nf.png
The problem
After a page reload, squaregroup is transformed (see image above), but the d3.event.transform of workspaceDiv is reset, meaning the first drag (of 1 pixel), resets transform (to 0,0) and not with the transform I've applied (200,400).
So the 2nd+ drag is fine, but the first drag throws all data off the page meaning you have to drag around until you find it.
Approaches
Attaching ".call(zoom)" on g means the draggable area is too small, and completely changes the behaviour for the user
I can't find a way to force update the tracking of a .event to be in sync after a page reload
I'm not sure if my approach is wrong, or if there is a function of d3.zoom I just can't find. Any input welcomed!
Many Thanks
Here is the solution (see it in a fiddle)
Make sure you are using zoomIdentity according to the D3 V5 and higher (the sample you tried to use is probably done for the previous versions):
const svg = d3.select('svg');
const group = svg.append('g');
group.append('circle').attr('r', 20);
const zoom = d3.zoom();
const onZoom = () => group.attr('transform', d3.event.transform);
zoom.on("zoom", onZoom);
svg.call(zoom);
const transform = d3.zoomIdentity;
transform.x = 100;
transform.y = 50;
group.call(zoom.transform, transform);

How to find links in d3 v4?

I have used the following code in d3 v3 to find nodes and links for a horizontal tree (top to bottom). Here is how the tree will look like:
var nodes = d3.layout.tree().nodes(jsonData).reverse();
var links = d3.layout.tree().links(nodes);
I'm trying to do the same in d3 v4. The v4 is missing the tree.links(nodes) method where it can accept the nodes.
How can I find the links in d3 v4?
I'm just adding this to save myself (and possibly others) time from having to dig the answers out of the demos. I'm new to D3 as well so I'm hoping this is as helpful to others as it was to me.
Use d3.hierarchy() & node.descendants() to get nodes and links.
// Assigns parent, children, height, depth, etc..
var root = d3.hierarchy(jsonData);
// Assigns the x and y coordinates for the nodes.
tree(root);
// Returns array of node objects.
var nodes = root.descendants();
// Returns array of link objects between nodes.
var links = root.descendants().slice(1);
//var links = root.links(); // to get objects with source and target properties.
Can also shorten this down a bit if wanted which still does all of the above.
var root = d3.hierarchy(jsonData),
nodes = tree(root).descendants(),
links = nodes.slice(1);
If you need to grab nodes/links within an event. For example on drag start of a node when using a tree diagram (and I'm sure this could be useful elsewhere as well).
var drag = d3.drag()
.on("start", function dragStart(d) {
var nodes = d.descendants(),
links = nodes.slice(1);
});
Or alternatively use node.links() where each link defines source and target properties.
var drag = d3.drag()
.on("start", function dragStart(d) {
var nodes = d.descendants(),
links = d.links();
});
You would think this would also work in this case but it doesn't (bummer).
var root = d3.hierarchy(jsonData),
nodes = tree(root).descendants(),
links = nodes.links(); // <-- only works on a single node, resort to slice(1).

d3 v4 Hierarchical Edge Bundling

I am trying to move my Hierarchical Edge Bundling chart to d3v4.
but could not find an alternative to d3.layout.bundle().
d3v3 example is https://bl.ocks.org/mbostock/7607999
any example with d3v4? Please help.
You'll need two things: the hierarchy and the array of links between nodes in the hierarchy.
You can load a hierarchy from JSON or create one from CSV by using d3.stratify. Then pass the hierarchy to d3.hierarchy. See the d3-hierarchy documentation.
Then you'll need to construct an array of links. Each link is an object with a source and target, each of which points to a node in the hierarchy.
You can calculate and draw the bundles using a combination of node.path (which replaces d3.layout.bundle) and d3.curveBundle. See the d3.curveBundle documentation.
If links is your array of links, the code looks something like this:
const line = d3.line()
.x(d => d.data.x)
.y(d => d.data.y)
.curve(d3.curveBundle.beta(0.95));
const edges = g.selectAll('.link').data(links);
edges.enter().append('path')
.attr('class', 'link')
.merge(edges)
.attr('d', d => line(d.source.path(d.target)));
edges.exit().remove();

How do I control the bounce entry of a Force Directed Graph in D3?

I've been able to build a Force Directed Graph using a Force Layout. Most features work great but the one big issue I'm having is that, on starting the layout, it bounces all over the page (in and out of the canvas boundary) before settling to its location on the canvas.
I've tried using alpha to control it but it doesn't seem to work:
// Create a force layout and bind Nodes and Links
var force = d3.layout.force()
.charge(-1000)
.nodes(nodeSet)
.links(linkSet)
.size([width/8, height/10])
.linkDistance( function(d) { if (width < height) { return width*1/3; } else { return height*1/3 } } ) // Controls edge length
.on("tick", tick)
.alpha(-5) // <---------------- HERE
.start();
Does anyone know how to properly control the entry of a Force Layout into its SVG canvas?
I wouldn't mind the graph floating in and settling slowly but the insane bounce of the entire graph isn't appealing, at all.
BTW, the Force Directed Graph example can be found at: http://bl.ocks.org/Guerino1/2879486enter link description here
Thanks for any help you can offer!
The nodes are initialized with a random position. From the documentation: "If you do not initialize the positions manually, the force layout will initialize them randomly, resulting in somewhat unpredictable behavior." You can see it in the source code:
// initialize node position based on first neighbor
function position(dimension, size) {
...
return Math.random() * size;
They will be inside the canvas boundary, but they can be pushed outside by the force. You have many solutions:
The nodes can be constrained inside the canvas: http://bl.ocks.org/mbostock/1129492
Try more charge strength and shorter links, or more friction, so the nodes will tend to bounce less
You can run the simulation without animating the nodes, only showing the end result http://bl.ocks.org/mbostock/1667139
You can initialize the nodes position https://github.com/mbostock/d3/wiki/Force-Layout#wiki-nodes (but if you place them all on the center, the repulsion will be huge and the graph will explode still more):
.
var n = nodes.length; nodes.forEach(function(d, i) {
d.x = d.y = width / n * i; });
I have been thinking about this problem too and this is the solution I came up with. I used nodejs to run the force layout tick offline and save the resulting nodes data to a json file.
I used that as the new json file for the layout. I'm not really sure it works better to be honest. I would like hear about any solutions you find.

Constraining d3 force layout graphs based on node degree

I have a force layout with potentially a very large number of nodes, too large for the graph to render responsively. I was thinking that one way to improve the performance of the system was to prune the graph by eliminating nodes based on in- and out-degree when the number of nodes gets too large.
Recomputing the node and link lists is a bit of a nuisance because links are related to indexes in the node array, and so all the links would need to be re-built.
It seems more elegant to be able to mark individual nodes for exclusion (analogously to the way some nodes are fixed) and have the layout algorithm skip those nodes. This would allow me to dynamically select subsets of the graph to show, while preserving as much state for each node (e.g., position) as practical.
Has anyone implemented something like this?
UPDATE:
I tried to implement the filter suggestion, but ran into an interesting error. It appears that the filter method returns an object that does not implement enter:
qChart apply limit:2
NODES BEF: [Array[218], enter: function, exit: function, select: function, selectAll: function, attr: function…]
NODES AFT: [Array[210], select: function, selectAll: function, attr: function, classed: function, style: function…]
Uncaught TypeError: Object [object Array] has no method 'enter'
The following code is run to get from BEF to AFT:
nodeSubset = nodeSubset.filter(function(n) { return (n.sentCount() <= limit); });
UPDATE 2:
I created a jsfiddle to isolate the problem. This example implements my interpretation of ChrisJamesC's answer. When I tried to implement his suggestion directly (putting the filter after the data), the subsequent call to enter failed because the object returned by filter did not have enter defined.
The goal is to make the layout select only those nodes that have active == true, so in this example, this means that node b should be excluded.
You can use the selection.filter() option combined with the node.weight attribute.
What you would normally do is:
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
Here you can do:
var node = svg.selectAll(".node")
.data(graph.nodes)
.filter(function(d){return d.weight>3})
.enter();
You might also have to remove from drawing the links going to these nodes using the same method.
EDIT You should just filter the data you provide if you want to mark nodes as active directly in the data array (and do the same for links)
var node = svg.selectAll(".node")
.data(force.nodes().filter(function(d) { return d.active; }));
var link = svg.selectAll(".link")
.data(force.links().filter(function(d) {
var show = d.source.active && d.target.active;
if (show)
console.log("kept", d);
else
console.log("excluded", d);
return show;
}) );
Fiddle
If you want to do this by computing the weight of each node, I would still recommend you to do this before passing the nodes and links to the graph and mark nodes as active or not following a specific criteria, then filter the links according to active nodes. Otherwise you would have to load the whole force directed layout only to get the weight to then filter the data to re-load the force directed graph.

Resources