Reputation: 33
I am using Mike Bostock's example of Hierarchical Edge Bundling in d3.js:
https://bl.ocks.org/mbostock/7607999
I am doing something slightly different: In my case nodes can have the same children multiple times e.g.:
[
{"name": A, "imports": [B, C, C, D]}
{"name": B, "imports": [A, C, D, D]}
{"name": C, "imports": [B, D]}
{"name": D, "imports": [B, A]}
]
This seems to be possible. However, the links are drawn right on top of teach other when they have the same source and target node. My visualization looks like this for now:
Now what I want to do is to change the curveBundle of the links slightly for each link that has the same source and target node so that all the links between these two nodes become visible (and not drawn right on top of each other).
What would be the best way to do this?
This is where the links are constructed:
let line = d3.radialLine()
.curve(d3.curveBundle.beta(0.85))
.radius(function(d) { return d.y; })
.angle(function(d) { return d.x / 180 * Math.PI; });
let link = canvas.append("g").selectAll(".link"),
node = canvas.append("g").selectAll(".node");
let root = packageHierarchy(arr)
.sum(function(d) { return d.size; });
cluster(root);
link = link
.data(packageImports(root.leaves()))
.enter().append("path")
.each(function(d) { d.source = d[0], d.target = d[d.length-1]; })
.attr("class", "link")
.attr("d", line);
This is the json object that contains my data:
Array (called 'arr' in the code)
Here is my total code:
function movementPath(arr) {
let diameter = 800,
radius = diameter / 2,
innerRadius = radius - 120;
let cluster = d3.cluster()
.size([360, innerRadius]);
let line = d3.radialLine()
.curve(d3.curveBundle.beta(0.85))
.radius(function(d) { return d.y; })
.angle(function(d) { return d.x / 180 * Math.PI; });
let link = canvas.append("g").selectAll(".link"),
node = canvas.append("g").selectAll(".node");
let root = packageHierarchy(arr)
.sum(function(d) { return d.size; });
cluster(root);
link = link
.data(packageImports(root.leaves()))
.enter().append("path")
.each(function(d) { d.source = d[0], d.target = d[d.length-1]; })
.attr("class", "link")
.attr("d", line);
node = node
.data(root.leaves())
.enter().append("text")
.attr("class", "node")
.attr("dy", "0.31em")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + (d.y + 8) + ",0)" + (d.x < 180 ? "" : "rotate(180)"); })
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.text(function(d) { return d.data.key; })
.on("mouseover", mouseovered)
.on("mouseout", mouseouted);
function mouseovered(d) {
node
.each(function(n) { n.target = n.source = false; });
link
.classed("link--target", function(l) { if (l.target === d) return l.source.source = true; })
.classed("link--source", function(l) { if (l.source === d) return l.target.target = true; })
.filter(function(l) { return l.target === d || l.source === d; })
.raise();
node
.classed("node--target", function(n) { return n.target; })
.classed("node--source", function(n) { return n.source; });
}
function mouseouted(d) {
link
.classed("link--target", false)
.classed("link--source", false);
node
.classed("node--target", false)
.classed("node--source", false);
}
// Lazily construct the package hierarchy from class names.
function packageHierarchy(classes) {
let map = {};
function find(name, data) {
let node = map[name], i;
if (!node) {
node = map[name] = data || {name: name, children: []};
if (name.length) {
node.parent = find(name.substring(0, i = name.lastIndexOf(".")));
node.parent.children.push(node);
node.key = name.substring(i + 1);
}
}
return node;
}
classes.forEach(function(d) {
find(d.name, d);
});
return d3.hierarchy(map[""]);
}
// Return a list of imports for the given array of nodes.
function packageImports(nodes) {
let map = {},
imports = [];
// Compute a map from name to node.
nodes.forEach(function(d) {
map[d.data.name] = d;
});
// For each import, construct a link from the source to target node.
nodes.forEach(function(d) {
if (d.data.imports) d.data.imports.forEach(function(i) {
imports.push(map[d.data.name].path(map[i]));
});
});
return imports;
}
}
Edit: after answer of martwetzels my visualization looks like this:
Upvotes: 3
Views: 614
Reputation: 437
The curve is only calculated once in your code. Replace the snippets within your code from below:
function lineConstructor () {
let manipulator = 0.7+(Math.random()*0.3)
console.log(manipulator)
return d3.radialLine()
.curve(d3.curveBundle.beta(manipulator))
.radius(function(d) { return d.y; })
.angle(function(d) { return d.x / 180 * Math.PI; });
}
link = link
.data(packageImports(root.leaves()))
.enter().append("path")
.each(function(d) { d.source = d[0], d.target = d[d.length - 1]; })
.attr("class", "link")
.each(function(d,i){
d3.select(this)
.attr("d", lineConstructor());
})
Instead of applying the curve to all elements, each element in the selection receives a random d3.curveBundle.beta()
value between 0.7 and 1. (feel free to change this). You can also pass the d,i parametes to the lineConstructor
if you want influence the parameter based on the data.
Upvotes: 2