Existing Triangles Cannot be Removed - d3.js

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>

Related

Add padding to sunburst layout d3 v4

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

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");
});
})

display axes in a circle with equal spacing in between

As you can see from this example by Mike Bostock, it is possible to display the axis to a graph in a circle. In the linked to example, there are three axis (with the areas in between of equal size) which seemed to be created by this line of code
var angle = d3.scale.ordinal().domain(d3.range(4)).rangePoints([0, 2 * Math.PI]),
plus
svg.selectAll(".axis")
.data(d3.range(3))
.enter().append("line")
.attr("class", "axis")
.attr("transform", function(d) { return "rotate(" + degrees(angle(d)) + ")"; })
.attr("x1", radius.range()[0])
.attr("x2", radius.range()[1]);
Playing around with that example, I was able to create a graph with six axes (with equal spacing between each axis) that covered the whole circle using this code
var angle = d3.scale.ordinal().domain(["one", "two", "three", "four", "five", "six"]).range([0, 45, 90, 135, 180, 225])
and then
svg.selectAll(".axis").data(d3.range(7))
//code omitted
However, I haven't been able to create a circle with 9 axes (with equal spacing in between) by doing this (as I expected it would)
var angle = d3.scale.ordinal()
.domain(["one", "two", "three", "four", "five", "six", "seven", "eight", "nine"])
.range([0, 45, 90, 135, 180, 225, 270, 315, 360]);
svg.selectAll(".axis").data(d3.range(10))
//code omitted
The result of doing this is that the axes start to go around the circle a second time.
Question: is there a pattern that can be followed to have an arbitrary number of axes displayed in a circle with equal spacing in between each? If so, please explain the d3 principles behind the two successful attempts and the one unsuccessful attempt shown and linked to above.
Update
Although removing the calls to degrees, puts the axes in the right position, (and then removing the call to degrees in the nodes code puts the nodes on the axes in the right axes), the links are not lining up properly i.e. they are not starting and ending on the axes, but rather floating unanchored. You can see the problem in this image
This is the code for the links (notice that it doesn't have a call to degrees)
svg.selectAll(".link")
.data(linx)
.enter().append("path")
.attr("class", "link")
.attr("class", function(d) { return "link " + d.Class})
.attr("d", link()
.angle(function(d) { return angle(d.X); })
.radius(function(d) { return radius(d.Y); }))
.on("mouseover", linkMouseover)
.on("mouseout", mouseout);
It calls a link function (which also doens't have a call to degrees, so I don't know why the links are starting and ending at the old position of the axes) that I got from Mike Bostock's hive implementation
function link() {
var source = function(d) { return d.Source; },
target = function(d) { return d.Target; },
angle = function(d) { return d.angle; },
startRadius = function(d) { return d.radius; },
endRadius = startRadius,
arcOffset = -Math.PI / 2;
function link(d, i) {
// console.log(d, i, "interior link func");
var s = node(source, this, d, i),
t = node(target, this, d, i),
x;
if (t.a < s.a) x = t, t = s, s = x;
if (t.a - s.a > Math.PI) s.a += 2 * Math.PI;
var a1 = s.a + (t.a - s.a) / 3,
a2 = t.a - (t.a - s.a) / 3;
return s.r0 - s.r1 || t.r0 - t.r1
? "M" + Math.cos(s.a) * s.r0 + "," + Math.sin(s.a) * s.r0
+ "L" + Math.cos(s.a) * s.r1 + "," + Math.sin(s.a) * s.r1
+ "C" + Math.cos(a1) * s.r1 + "," + Math.sin(a1) * s.r1
+ " " + Math.cos(a2) * t.r1 + "," + Math.sin(a2) * t.r1
+ " " + Math.cos(t.a) * t.r1 + "," + Math.sin(t.a) * t.r1
+ "L" + Math.cos(t.a) * t.r0 + "," + Math.sin(t.a) * t.r0
+ "C" + Math.cos(a2) * t.r0 + "," + Math.sin(a2) * t.r0
+ " " + Math.cos(a1) * s.r0 + "," + Math.sin(a1) * s.r0
+ " " + Math.cos(s.a) * s.r0 + "," + Math.sin(s.a) * s.r0
: "M" + Math.cos(s.a) * s.r0 + "," + Math.sin(s.a) * s.r0
+ "C" + Math.cos(a1) * s.r1 + "," + Math.sin(a1) * s.r1
+ " " + Math.cos(a2) * t.r1 + "," + Math.sin(a2) * t.r1
+ " " + Math.cos(t.a) * t.r1 + "," + Math.sin(t.a) * t.r1;
}
function node(method, thiz, d, i) {
var node = method.call(thiz, d, i),
a = +(typeof angle === "function" ? angle.call(thiz, node, i) : angle) + arcOffset,
r0 = +(typeof startRadius === "function" ? startRadius.call(thiz, node, i) : startRadius),
r1 = (startRadius === endRadius ? r0 : +(typeof endRadius === "function" ? endRadius.call(thiz, node, i) : endRadius));
return {r0: r0, r1: r1, a: a};
}
link.source = function(_) {
if (!arguments.length) return source;
source = _;
return link;
};
link.target = function(_) {
if (!arguments.length) return target;
target = _;
return link;
};
link.angle = function(_) {
if (!arguments.length) return angle;
angle = _;
return link;
};
link.radius = function(_) {
if (!arguments.length) return startRadius;
startRadius = endRadius = _;
return link;
};
link.startRadius = function(_) {
if (!arguments.length) return startRadius;
startRadius = _;
return link;
};
link.endRadius = function(_) {
if (!arguments.length) return endRadius;
endRadius = _;
return link;
};
return link;
}
You have angles in degrees already, you don't need to use the degree() function. If you remove the call to that everything will work fine.

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.

Resources