Avoid crossing links in force-directed graph (d3.js) - d3.js

Using d3.js, I have this force-directed-graph situation:
let simulation = d3.forceSimulation(nodes);
simulation
.force("center", d3.forceCenter(width / 2, height / 2))
.force("nodes", d3.forceManyBody())
.force(
"links",
d3
.forceLink(links)
.id(d => d.id)
.distance(d => 5 * (d.source.size + d.target.size))
)
.on("tick", ticked);
this creates a repelling force between nodes. My goal: I would like to find a way to tell d3 to avoid crossing links - perhaps create an "artificial" midpoint element in the middle of the links and then repel those midpoints? One possibility.
In the above image, we can see that the blue node link is crossing/overlapping. I am seeking to discourage links from crossing/intersecting.
How can I accomplish this?

Related

increase force between nodes that are not linked

I have a force-directed graph using d3.js, part of the code is like:
simulation
.force("center", d3.forceCenter(width / 2, height / 2))
.force("nodes", d3.forceManyBody())
.force(
"links",
d3
.forceLink(links)
.id(d => d.id)
.distance(d => 5 * (d.source.size + d.target.size))
)
.on("tick", ticked);
this line determines the force between linked nodes:
.distance(d => 5 * (d.source.size + d.target.size))
however, I would like to provide a force between unlinked nodes (ideally, the force would increase as the degree of freedom increases).
How can I accomplish this?
these seem to do the trick:
.force("charge", d3.forceManyBody().strength(-98))
.force("collide", d3.forceCollide().radius(50))
how can I change these forces dynamically as the animation progresses?

Box plot with dual dimension and dual x-axis using dc.js

Is it possible with dc.js to draw two x-axis of a graph i.e. one is below and one is above. One Dimension/ x-axis contain a b and above x-axis contain 1 (a b with below a-axis) 2 (a b with below x-axis). An img is attached to explain the view. If it is possible kindly give some suggestion.
Regards.
As for adding lines between the box plots, here is a hacky solution that works ok. Would probably need some work to make it general.
Assume we have the domain (['1A', '1B', '2A, '2B', ...]) in a variable called domain.
We can add a pretransition handler that draws lines after every second box:
function x_after(chart, n) {
return (chart.x()(domain[n]) + chart.x()(domain[n+1])) / 2 + chart.margins().left + 7; // why 7?
}
chart.on('pretransition', chart => {
let divide = chart.g().selectAll('line.divide').data(d3.range(domain.length/2));
divide.exit().remove();
divide = divide.enter()
.append('line')
.attr('class', 'divide')
.attr('stroke', 'black')
.merge(divide);
divide
.attr('x1', n => x_after(chart, n*2 + 1))
.attr('x2', n => x_after(chart, n*2 + 1))
.attr('y1', chart.margins().top)
.attr('y2', chart.margins().top + chart.effectiveHeight())
})
This uses the D3 general update pattern to add a vertical line after every other box (specifically those with odd index number).
It takes the average of the X position of 1B and 2A, 2B and 3A, etc. I have no idea why I had to add 7, so probably I am missing something.
demo fiddle.

d3-force: keep graph compact when filtering/removing nodes

I use d3-force to lay out a graph with about 360 nodes.
const simulation = d3.forceSimulation(nodes)
.force(
'charge',
d3.forceManyBody()
.distanceMax(200)
.strength(-50)
)
.force(
'link',
d3.forceLink(links)
.id((d) => d.id)
.distance(30)
)
.force(
'center',
d3.forceCenter(
$svg.innerWidth() / 2,
$svg.innerHeight() / 2,
)
);
this looks good with all nodes visible – but there will also be the possibility to filter/remove nodes, in which case I would want the graph to be way more compact than it actually is (see animation).
this is probably due to the fact that there are no edges between the remaining nodes, and the fact that they are already spread out a lot when the new simulation starts.
while I could simply reset all node positions to the center of the canvas, that would not look great transition-wise. ideally each node would move from its current position to its new position in a more compact layout.
is there a way to achieve this?
I thought maybe the forceManyBody strength could transition from a positive value (attraction) at first to a negative value (repulsion), but apparently this value is can only be set once for the run of the simulation.
adding an attraction force (https://github.com/ericsoco/d3-force-attract) works well enough.

How to visualize groups of nodes in a d3 force-directed graph layout

I'm looking for a way to plug in groups to my force-directed graph visualization. I've found three related examples so far:
Cola.js which would require adding another library and possibly retro-fitting my code to fit this different library.
This block, which is pretty hard to untangle.
This slide from mbostock's slide deck, which isn't what I want but on the right path...
What I'd like most is a simple way of adding something very close to the structure from the first link, but without too much overhead.
Right now I have a pretty standard setup:
var link = g.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style(...
var node = g.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return d.id; })
I was hoping to just grab the d3 code out of cola.js and mess with it, but that library seems fairly complicated so it wouldn't be too easy. I'm hoping it isn't too hard to get something kind of like this in straight d3:
Thanks!
I'm following the title "visualize groups of nodes" more than the suggested picture, but I think it wouldn't be that hard to tweak my answer to show bounding boxes as in the image
There's probably a few d3 only solutions, all of them almost certainly require tweaking the node positions manually to keep nodes grouped properly. The end result won't strictly be typical of a force-layout because links and node positions must be manipulated to show grouping in addition to connectivity - consquently, the end result will be a compromise between each force - node charge, length strength and length, and group.
The easiest way to accomplish your goal may be to:
Weaken link strength when links link different groups
On each tick, calculate each group's centroid
Adjust each node's position to move it closer to the group's centroid
Use a voronoi diagram to show the groupings
For my example here, I'll use Mike's canonical force layout.
Weaken links when links link different groups
Using the linked example, we can dampen the link strength when link target and link source have different groups. The specified strength will likely need to be altered depending on the nature of the force layout - more inter-connected groups will likely need to have weaker intergroup link strength.
To change the link strength depending on if we have an intergroup link or not, we might use:
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(function(link) {
if (link.source.group == link.source.target) {
return 1; // stronger link for links within a group
}
else {
return 0.1; // weaker links for links across groups
}
}) )
.force("charge", d3.forceManyBody().strength(-20))
.force("center", d3.forceCenter(width / 2, height / 2));
On Each Tick, Calculate Group Centroids
We want to force group nodes together, to do so we need to know the centroid of the group. The data structure of simulation.nodes() isn't the most amenable to calculating centroids, so we need to do a bit of work:
var nodes = this.nodes();
var coords ={};
var groups = [];
// sort the nodes into groups:
node.each(function(d) {
if (groups.indexOf(d.group) == -1 ) {
groups.push(d.group);
coords[d.group] = [];
}
coords[d.group].push({x:d.x,y:d.y});
})
// get the centroid of each group:
var centroids = {};
for (var group in coords) {
var groupNodes = coords[group];
var n = groupNodes.length;
var cx = 0;
var tx = 0;
var cy = 0;
var ty = 0;
groupNodes.forEach(function(d) {
tx += d.x;
ty += d.y;
})
cx = tx/n;
cy = ty/n;
centroids[group] = {x: cx, y: cy}
}
Adjust each node's position to move it closer to its group's centroid:
We don't need to adjust every node - just those that are straying fairly far from their centroids. For those that are sufficiently far we can nudge them closer using a weighted average of the centroid and the node's current position.
I modify the minimum distance used to determine if a node should be adjusted as the visualization cools. For the majority of the time when the visualization is active, when alpha is high, the priority is grouping, so most nodes will be forced towards the grouping centroid. As alpha drops towards zero, nodes should be grouped already, and the need to coerce their position is less important:
// don't modify points close the the group centroid:
var minDistance = 10;
// modify the min distance as the force cools:
if (alpha < 0.1) {
minDistance = 10 + (1000 * (0.1-alpha))
}
// adjust each point if needed towards group centroid:
node.each(function(d) {
var cx = centroids[d.group].x;
var cy = centroids[d.group].y;
var x = d.x;
var y = d.y;
var dx = cx - x;
var dy = cy - y;
var r = Math.sqrt(dx*dx+dy*dy)
if (r>minDistance) {
d.x = x * 0.9 + cx * 0.1;
d.y = y * 0.9 + cy * 0.1;
}
})
Use a Voronoi Diagram
This allows the easiest grouping of nodes - it ensures that there is no overlap between group shells. I haven't built in any verification to ensure that a node or set of node's aren't isolated from the rest of their group - depending on the visualization's complexity you might need this.
My initial thought was using a hidden canvas to calculate if shells overlapped, but with a Voronoi you could probably calculate if each group is consolidated using neighboring cells. In the event of non-consolidated groups you could use a stronger coercion on stray nodes.
To apply the voronoi is fairly straightforward:
// append voronoi
var cells = svg.selectAll()
.data(simulation.nodes())
.enter().append("g")
.attr("fill",function(d) { return color(d.group); })
.attr("class",function(d) { return d.group })
var cell = cells.append("path")
.data(voronoi.polygons(simulation.nodes()))
And update on each tick:
// update voronoi:
cell = cell.data(voronoi.polygons(simulation.nodes())).attr("d", renderCell);
Results
Altogether, this looks like this during the grouping phase:
And as the visualization finally stops:
If the first image is preferable, then remove the part the changes the minDistance as alpha cools down.
Here's a block using the above method.
Further Modification
Rather than using the centroid of each group's nodes, we could use another force diagram to position the ideal centroid of each group. This force diagram would have a node for each group, the strength of links between each group would correspond to te number of links between the nodes of the groups. Using this force diagram, we could coerce the original nodes towards our idealized centroids - the nodes of the second force layout.
This approach may have advantages in certain situations, such as by separating groups by greater amounts. This approach might give you something like:
I've included an example here, but hope that the code is commented sufficiently to understand without a breakdown like the above code.
Block of second example.
The voronoi is easy, but not always the most aesthetic, you could use a clip path to keep clip the polygons to some sort of oval, or use a gradient overlay to fade the polygons out as they reach the edges. One option that is likely possible depending on graph complexity is using a minimum convex polygon instead, though this won't work well with groups with less than three nodes. Bounding box's probably won't work in most instances, unless you really keep the coercion factor high (eg: keep minDistance very low the entire time). The trade off will always be what do you want to show more: connections or grouping.

d3 issues with entering elements after pan

I use a transform when panning, copied from several examples.
zoom = d3.behavior.zoom()
.x(this.xScale)
.scaleExtent([0.5, 2])
.on("zoom", zoomFunction(this))
.on("zoomend", zoomEndFunction(this));
svg = histogramContainer.append("svg")
.attr('class', 'chart')
.attr('width', width)
.attr('height', height)
.call(zoom)
.append('g')
.attr('transform', 'translate(' + this.margin.left + ' , ' +
(height - this.margin.bottom) + ')');
function zoomFunction(scope) {
return function() {
var that = scope;
that.xDelta = d3.event.translate[0];
that.zoomScale = d3.event.scale;
// some other code removed for simplicity
svg.selectAll(".stackedBar").attr("transform", "translate(" +
that.xDelta + ",0)scale(" +
that.zoomScale + ", 1)");
};
}
The problem is that since new elements enter after the pan then 'old' elements have the transform attribute applied but the new elements don't.
This breaks future panning because the old elements will be transformed from where the pre-zoom xScale drew them while the new elements will be transformed from the zoom-adjusted xScale.
It seems to me that I could redraw the old elements with the zoom-adjusted xScale, though I'm unsure when and how to do that "behind the scenes".
Alternatively I could draw the new elements with the old xScale and apply the same transform on them that the old elements have. This seems messier since elements will come and go and I'll have to keep track of the 'current transform'. My gut tells me "too much state".
Usually if you're attaching a scale to the zoom behaviour, you use the modified scale to redraw the bars using the exact same code as how you position the bars initially, letting the scales do all the work.
I linked to this discussion in my previous answer, so you might have read it by now; if not, it might be a good start for getting your head around the different ways of approaching zooming in d3; it breaks down the each method step-by-step. You're currently using a mix of two approaches (transforms versus scales), and I think that's causing problems keeping track.

Resources