Reputation: 1317
I'm trying to understand how this beautiful example works...
http://bl.ocks.org/mbostock/1804919
I see that clustering is done by the color of the nodes, but I'm confused by the line in question in the collision detection function...
r = d.radius + quad.point.radius + (d.color !== quad.point.color) * padding;
How can you "add" the product of a comparison of the colors "d.color" and "quad.point.color"? I would have assumed this would return nothing more than a true/false? Either way, I'm not sure I follow only this reference to color will have the desired effect of clustering by color?
Anyway, I haven't been able to find any line-by-line description of the workings of the collision detection function, so I'm really hoping that someone here understands it well enough to help explain this bit to me.
All I'm ultimately trying to achieve is to adapt the example to cluster by another non-numeric node attribute (e.g. d.person_name !== quad.point.person_name).
Thanks!
Upvotes: 2
Views: 1067
Reputation: 6476
The line you are asking about is calculating the allowable distance between nodes, the distance between nodes (l
) is compared to r
to determine if there is a collision between d
and quad.point
. The value padding
is added to the allowable distance between nodes if they are of the same colour. The boolean result is coerced into a Number type by the context.
Instead of assuming what JS does its really easy to open the browser tools and just type the expression in to see what the result is...
But the collision detection is not involved in the clustering, that is handled by this code...
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
If you have some data and you want to use the same code to group them by a particular attribute, then you need to add a cx
and cy
property to your data such that items with the same attribute value (the value does not need to be numeric) have the same cx
and cy
values.
var width = 600,
height = 200,
padding = 6, // separation between nodes
maxRadius = 6;
var n = 200, // total number of nodes
names = ["Givens", "Crowder", "Lannister", "Baratheon", "Stark"],
m = names.length; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
var x = d3.scale.ordinal()
.domain(names)
.rangePoints([0, width], 1),
legend = d3.svg.axis()
.scale(x)
.orient("top")
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
v = (i + 1) / m * -Math.log(Math.random());
return {
radius: Math.sqrt(v) * maxRadius,
color: color(i),
cx: x(names[i]),
cy: height / 2
};
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(0)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height),
gLegend = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + height * 0.9 + ")")
.call(legend);
gLegend.selectAll(".tick text")
.attr("fill", function(d, i) {
return color(i);
});
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", function(d) {
return d.radius;
})
.style("fill", function(d) {
return d.color;
})
.call(force.drag);
function tick(e) {
circle
.each(gravity(.2 * e.alpha))
.each(collide(.5))
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + padding,
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.color !== quad.point.color) * padding;
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;
});
};
}
circle {
stroke: #000;
}
.x.axis path {
fill: none;
}
.x.axis text {
font-family: Papyrus, Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, sans-serif;
}
body {
background-color: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
Upvotes: 6