Add padding to sunburst layout d3 v4 - d3.js

Following this example I am creating a sunburst diagram. I would like to add a bit of padding between each node. Here is the code I have so far:
var sunburstLayout = d3.partition();
var radius = 100;
sunburstLayout.size([2*Math.PI, radius]);
//sunburstLayout.padding(2);
var arc= d3.arc()
.startAngle( function(d) { return d.x0 })
.endAngle( function(d) { return d.x1 })
.innerRadius(function(d) { return d.y0 })
.outerRadius(function(d) { return d.y1 })
root.sum(d => d.value);
...
which produces this:
Now when adding:
sunburstLayout.padding(2);
The layout is messed up.
sunburstLayout.padding(1);
Any suggestions on how to keep the layout correct while being able to add padding to each node?

It seems to me that you just found a limitation of this padding method. If you look at the source code...
function positionNode(dy, n) {
return function(node) {
if (node.children) {
treemapDice(node, node.x0, dy * (node.depth + 1) / n, node.x1, dy * (node.depth + 2) / n);
}
var x0 = node.x0,
y0 = node.y0,
x1 = node.x1 - padding,
y1 = node.y1 - padding;
if (x1 < x0) x0 = x1 = (x0 + x1) / 2;
if (y1 < y0) y0 = y1 = (y0 + y1) / 2;
node.x0 = x0;
node.y0 = y0;
node.x1 = x1;
node.y1 = y1;
};
}
... you'll see that it works for linear distances, but it doesn't work for radial layouts, where x and y don't represent a cartesian coordinate. Therefore, it can be used for icicles, but nor for sunbursts.
A quick and dirty workaround is changing the arc generator instead, using padAngle and adding the linear value in both innerRadius and outerRadius (in innerRadius you add the value, and in outerRadius you subtract the value). That gives us:
This is the updated CodePen: https://codepen.io/anon/pen/QBWMKm?editors=0010

Related

Existing Triangles Cannot be Removed

I bind triangles with data in D3.js. But the triganle cannot be removed with the data. The rectangles are okay. Complete code is attached!
var svg = d3.select("body").append("svg")
.attr("width", 250)
.attr("height", 250);
function render(data){
var tris = svg.selectAll("tri").data(data);
tris.enter().append("path");
tris.attr("d", function(d) {
var x1 = (0.4 - 0.2 * (d - 1)) * 250, y1 = 0.3 * 250;
var x2 = (0.5 - 0.2 * (d - 1)) * 250, y2 = 0.1 * 250;
var x3 = (0.6 - 0.2 * (d - 1)) * 250, y3 = 0.3 * 250;
return "M" + x1 + " " + y1 + " L" + x2 + " " + y2 + " L" + x3 + " " + y3 + "Z";
});
tris.exit().remove();
var rects = svg.selectAll("rect").data(data);
rects.enter().append("rect");
rects.attr("y", 50)
.attr("width", 20)
.attr("height", 20)
.attr("x", function(d) { return d * 40; });
rects.exit().remove();
}
render([1, 2, 3]);
render([1, 2]);
render([1]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
There is no SVG element called <tri>. Your enter selection works because you can select anything in your data binding function. As there is nothing named "tri" in the whole DOM (be it a tag, a class, an ID, whatever...), your enter selection is never empty and your exit selection is never populated.
That being said, an easy solution is selecting by class...
var tris = svg.selectAll(".tri").data(data);
And setting this class in your enter selection:
tris.enter().append("path").attr("class", "tri");
Here is your code with the changes:
var svg = d3.select("body").append("svg")
.attr("width", 250)
.attr("height", 250);
function render(data){
var tris = svg.selectAll(".tri").data(data);
tris.enter().append("path").attr("class","tri");
tris.attr("d", function(d) {
var x1 = (0.4 - 0.2 * (d - 1)) * 250, y1 = 0.3 * 250;
var x2 = (0.5 - 0.2 * (d - 1)) * 250, y2 = 0.1 * 250;
var x3 = (0.6 - 0.2 * (d - 1)) * 250, y3 = 0.3 * 250;
return "M" + x1 + " " + y1 + " L" + x2 + " " + y2 + " L" + x3 + " " + y3 + "Z";
});
tris.exit().remove();
var rects = svg.selectAll("rect").data(data);
rects.enter().append("rect");
rects.attr("y", 50)
.attr("width", 20)
.attr("height", 20)
.attr("x", function(d) { return d * 40; });
rects.exit().remove();
}
render([1, 2, 3]);
setTimeout(() => render([1, 2]), 1000);
setTimeout(() => render([1]), 2000)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Trying to get only one tooltip to show on mouseover using d3

I am trying get some tooltips working on my d3 code. I've simplified this code as shown below.
Currently the code produces 6 moving circles with names associated to them, and a console.log shows that when I mouseover over each circle, it's associated name is logged.
Also, when I mouseover a circle, the name labels appear near to the circles. However, the labels appear over all of the circles, and I only want a label to appear over the circle that the cursor is hovering over.
I'm not interested in adding a mouseout or anything just yet, just want to get it so that only the hovered over circle gets a label displayed. Any advice on how to do this?
Here is what my code currently looks like:
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script type="text/javascript">
var names = ["Jack","Anne","Jacob","Mary","Michael","Lisa"];
var width = 960,
height = 500;
var n = names.length,
m = 12,
degrees = 180 / Math.PI;
var bubbles = d3.range(n).map(function() {
var x = Math.random() * width,
y = Math.random() * height;
return {
vx: Math.random() * 2 - 1,
vy: Math.random() * 2 - 1,
path: d3.range(m).map(function() { return [x, y]; }),
count: 0
};
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.selectAll("g")
.data(bubbles)
.enter().append("g")
.on("mouseover", function(d,i){console.log(names[i])});
var labels = g.selectAll("text")
.data(bubbles)
.enter().append("text")
.attr("dy",".35em")
.attr("class", "tooltip")
.style("visibility", "hidden")
.text(function(d,i){return names[i]})
var head = g.selectAll("circle")
.data(bubbles)
.enter().append("circle")
.attr("r", 6)
.on("mouseover", function(d,i) {
labels.style("visibility","visible")
})
d3.timer(function() {
for (var i = -1; ++i < n;) {
var bubble = bubbles[i],
path = bubble.path,
dx = bubble.vx,
dy = bubble.vy,
x = path[0][0] += dx,
y = path[0][1] += dy,
speed = Math.sqrt(dx * dx + dy * dy),
count = speed * 10,
k1 = -5 - speed / 3;
if (x < 0 || x > width) bubble.vx *= -1;
if (y < 0 || y > height) bubble.vy *= -1;
for (var j = 0; ++j < m;) {
var vx = x - path[j][0],
vy = y - path[j][1],
k2 = Math.sin(((bubble.count += count) + j * 3) / 300) / speed;
path[j][0] = (x += dx / speed * k1) - dy * k2;
path[j][1] = (y += dy / speed * k1) + dx * k2;
speed = Math.sqrt((dx = vx) * dx + (dy = vy) * dy);
}
}
labels.attr("transform", labelsTransform);
head.attr("transform", headTransform);
});
function headTransform(d) {
return "translate(" + d.path[0] + ")rotate(" + Math.atan2(d.vy, d.vx) * degrees + ")";
}
function labelsTransform(d) {
return "translate(" + d.path[0] + ")translate(10)";
}
</script>
<body>
</html>
This is my first question on Stack Overflow, so sorry if my question is badly formatted or presented! Any help would be much appreciated, even if it's about how to better present my question!
Welcome to Stack Overflow! :)
The issue you're having can be traced back to this section of code:
var head = g.selectAll("circle")
.data(bubbles)
.enter().append("circle")
.attr("r", 6)
.on("mouseover", function(d,i) {
labels.style("visibility","visible")
})
What this says is that when the user mouseovers any circle, make all labels visible. You probably want something like this
.on("mouseover", function(d,i) {
// only make visible the current mouseover-ed point
labels.filter(function(p){
if(p === d) d3.select(this).style("visibility","visible");
else d3.select(this).style("visibility","hidden");
});
})

Specifying a number of nodes in each cluster of clustered force layout in d3js

I am trying to design an infographic using the Clustered Force Layout III example and I have it almost the way I want, but I need to specify a number of nodes per cluster. Cluster 1 - 10 nodes, Cluster 2 - 7 nodes, Cluster 3 - 11 nodes, Cluster 4 - 18 nodes, Cluster 5 - 16 nodes, Cluster 6 - 19 nodes, Cluster 7 - 42 nodes, Cluster 8 - 14 nodes. I have the correct total number of nodes and the correct total number of Clusters and I've made all the nodes the same size. Now I just need to push the circles around to represent by data. My code is below.
var width = 175,
height = 175,
padding = 1.5, // separation between same-color nodes
clusterPadding = 6, // separation between different-color nodes
maxRadius = 4;
var n = 137, // total number of nodes
m = 8; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
// The largest node for each cluster.
var clusters = new Array(m);
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
r = maxRadius,
d = {
cluster: i,
radius: r,
x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
return d;
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select(".wpd3-1042-0").append("svg")
.attr("width", width)
.attr("height", height);
var node = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.style("fill", function(d) { return color(d.cluster); })
.call(force.drag);
node.transition()
.duration(750)
.delay(function(d, i) { return i * 5; })
.attrTween("r", function(d) {
var i = d3.interpolate(0, d.radius);
return function(t) { return d.radius = i(t); };
});
function tick(e) {
node
.each(cluster(10 * e.alpha * e.alpha))
.each(collide(.5))
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// Move d to be adjacent to the cluster node.
function cluster(alpha) {
return function(d) {
var cluster = clusters[d.cluster];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + cluster.radius;
if (l != r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
};
}
// Resolves collisions between d and all other circles.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + Math.max(padding, clusterPadding),
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + (d.cluster === quad.point.cluster ? padding : clusterPadding);
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
Yes! you can achieve this by doing something like this:
var clusterNumber = [10, 7, 11, 18, 16, 19, 42, 14];//your cluster number of node array
var n = d3.sum(clusterNumber, function (d) {
return d
}); /// total number of nodes
var m = clusterNumber.length;//total number of clusters
var color = d3.scale.category10()
.domain(d3.range(m));
// The largest node for each cluster.
var clusters = new Array(m);
var nodes = [];
clusterNumber.forEach(function (cn, i) {
//this will make a cluster
var r = maxRadius;
for (var j = 0; j < cn; j++) {
//this loop will make all the nodes
var d = {
cluster: i,
radius: r,
x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
nodes.push(d);
}
});
Full working code here
Hope this helps!

Can one specify a custom force function for a force-directed layout?

I want to experiment with an alternative family force functions for force-directed graph layouts.
For each node n_i, I can define a "force function" f_i such that
f_i ( n_i ) is identically zero; and
f_i ( n_j ), where n_i != n_j, is the force on node n_i that is due to some other node n_j.
The net force on node n_i should then be the vector sum of the forces f_i ( n_j ), where n_j ranges over all other nodes1.
Is there some way to tell d3.js to use these custom force functions in the layout algorithm?
[The documentation for d3.js's force-directed layout describes various ways in which its built-in force function can be tweaked, but I have not been able to find a way to specify an entirely different force function altogether, i.e. a force function that cannot be achieved by tweaking the parameters of the built-in force function.]
1IOW, no other/additional forces should act on node n_i besides those computed from its force function f_i.
Yes you can. Credit goes to Shan Carter and his bl.ocks example
let margin = {
top: 100,
right: 100,
bottom: 100,
left: 100
};
let width = 960,
height = 500,
padding = 1.5, // separation between same-color circles
clusterPadding = 6, // separation between different-color circles
maxRadius = 12;
let n = 200, // total number of nodes
m = 10, // number of distinct clusters
z = d3.scaleOrdinal(d3.schemeCategory20),
clusters = new Array(m);
let svg = d3.select('body')
.append('svg')
.attr('height', height)
.attr('width', width)
.append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
let nodes = d3.range(200).map(() => {
let i = Math.floor(Math.random() * m),
radius = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {
cluster: i,
r: radius
};
if (!clusters[i] || (radius > clusters[i].r)) clusters[i] = d;
return d;
});
let circles = svg.append('g')
.datum(nodes)
.selectAll('.circle')
.data(d => d)
.enter().append('circle')
.attr('r', (d) => d.r)
.attr('fill', (d) => z(d.cluster))
.attr('stroke', 'black')
.attr('stroke-width', 1);
let simulation = d3.forceSimulation(nodes)
.velocityDecay(0.2)
.force("x", d3.forceX().strength(.0005))
.force("y", d3.forceY().strength(.0005))
.force("collide", collide) // <<-------- CUSTOM FORCE
.force("cluster", clustering)//<<------- CUSTOM FORCE
.on("tick", ticked);
function ticked() {
circles
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y);
}
// Custom 'clustering' force implementation.
function clustering(alpha) {
nodes.forEach(function(d) {
var cluster = clusters[d.cluster];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.r + cluster.r;
if (l !== r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
});
}
// Custom 'collide' force implementation.
function collide(alpha) {
var quadtree = d3.quadtree()
.x((d) => d.x)
.y((d) => d.y)
.addAll(nodes);
nodes.forEach(function(d) {
var r = d.r + maxRadius + Math.max(padding, clusterPadding),
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.data && (quad.data !== d)) {
var x = d.x - quad.data.x,
y = d.y - quad.data.y,
l = Math.sqrt(x * x + y * y),
r = d.r + quad.data.r + (d.cluster === quad.data.cluster ? padding : clusterPadding);
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.data.x += x;
quad.data.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
});
}
<!doctype html>
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
Also here is a more in-depth look at the subject.
To achieve this, you'll need to create your own custom layout. There's no tutorial for this that I'm aware of, but the source code for the existing force layout should be a good starting point as, by the sound of it, the structure of your custom layout would be very similar to that.

D3 Force Layout Graph - Self linking node

Using a force directed graph, how do you get a link to actually show up when the target and source are the same node. So basically just a nice little loop indicating that such an edge exists.
There are two D3 examples that I already used or tried to use:
I'm using http://bl.ocks.org/d3noob/5155181 to show direction, and
the little endpoint arrow will show up pointing at itself, but no
link line.
http://bl.ocks.org/GerHobbelt/3616279 does does allow for self
referencing and I even sort of got it to work with my data, but it is
crazy complicated.
The trick is to draw the self link as a path with an arc in it. It took me a bit of fiddling with the arc parameter syntax to get things working and the key seemed to be that the arc could not start and end at the same point. Here is the relevant code that draws the edges at each update.
function tick() {
link.attr("d", function(d) {
var x1 = d.source.x,
y1 = d.source.y,
x2 = d.target.x,
y2 = d.target.y,
dx = x2 - x1,
dy = y2 - y1,
dr = Math.sqrt(dx * dx + dy * dy),
// Defaults for normal edge.
drx = dr,
dry = dr,
xRotation = 0, // degrees
largeArc = 0, // 1 or 0
sweep = 1; // 1 or 0
// Self edge.
if ( x1 === x2 && y1 === y2 ) {
// Fiddle with this angle to get loop oriented.
xRotation = -45;
// Needs to be 1.
largeArc = 1;
// Change sweep to change orientation of loop.
//sweep = 0;
// Make drx and dry different to get an ellipse
// instead of a circle.
drx = 30;
dry = 20;
// For whatever reason the arc collapses to a point if the beginning
// and ending points of the arc are the same, so kludge it.
x2 = x2 + 1;
y2 = y2 + 1;
}
return "M" + x1 + "," + y1 + "A" + drx + "," + dry + " " + xRotation + "," + largeArc + "," + sweep + " " + x2 + "," + y2;
});
And here is a jsfiddle that demonstrates the whole thing, and a screenshot:

Resources