D3 Force Graph - Fixed Nodes that Don't Overlap - javascript

I am looking to develop a viz that consists of a node link graph. I have a series of points whose position I don't want to change unless there is a collision (one node on another) on the graph. In case of collided nodes, I want to space them so that they don't overlap. My JS code is as below
var chartWidth = 200;
var chartHeight = 200;
var widthPadding = 40;
var heightPadding = 40;
var link, node;
$(function(){
initialize();
});
function initialize() {
var jsonString = '{"nodes":[{"x":40,"y":64,"r":6,"fixed":true},{"x":40,"y":63,"r":6,"fixed":true},{"x":119,"y":53,"r":6,"fixed":true},{"x":119,"y":73,"r":6,"fixed":true},{"x":137,"y":73,"r":6,"fixed":true},{"x":140,"y":140,"r":6,"fixed":true},{"x":68,"y":57,"r":6,"fixed":true},{"x":70,"y":75,"r":6,"fixed":true},{"x":51,"y":59,"r":6,"fixed":true},{"x":51,"y":54,"r":6,"fixed":true},{"x":137,"y":40,"r":6,"fixed":true}],"links":[{"source":0,"target":1},{"source":1,"target":2},{"source":2,"target":3},{"source":3,"target":4},{"source":4,"target":5},{"source":0,"target":1},{"source":1,"target":6},{"source":6,"target":7},{"source":7,"target":4},{"source":4,"target":5},{"source":0,"target":1},{"source":1,"target":8},{"source":8,"target":9},{"source":9,"target":10},{"source":10,"target":5}]}';
drawForceDirectedNodeLink($.parseJSON(jsonString));
}
function drawForceDirectedNodeLink(graph){
var width = chartWidth + (2*widthPadding);
var height = chartHeight + (2*heightPadding);
var q = d3.geom.quadtree(graph.nodes),
i = 0,
n = graph.nodes.length;
while (++i < n) {
q.visit(collide(graph.nodes[i]));
}
var force = d3.layout.force()
.size([width, height])
.gravity(0.05)
.on("tick", function(){
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) { return d.r; });
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node");
}
function collide(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * .5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2
|| x2 < nx1
|| y1 > ny2
|| y2 < ny1;
};
}
As you can see, I have tried to implement the collision detection logic mentioned here. But some how I have not been able to get that part work.

Notice that within your jsonString declaration inside of initialize(), each node is being given an r property. However, further down within collide(), you're doing the following:
.attr("r", function(d) { return d.radius - 2; })
Make sure your nodes have a radius property attached to them. If not, the following change should do it:
.attr("r", function(d) { return d.r - 2; })
You can see on line 30 of Mike Bostock's script that his nodes are initially declared with a radius property, as opposed to your r property.
var nodes = d3.range(200).map(function() { return {radius: Math.random() * 12 + 4}; }),

UPDATE
Change node.radius to node.r and quad.point.radius to quad.point.r. And it should work. Looks like it was just a NaN problem.

Related

After a few clicks the animation freezes, why?

I have the following code, which I took and modify from here
Here is my jsfiddle in action
I am currently developing a user interaction that needs these bubbles, exactly 5 bubbles that gravitate to the center of the screen. The thing is that I know how many times a user will click on each of these bubbles. The thing I noticed is that at some point the bubbles keep growing but the collision among them will stop working.
Here you can see the code that modify:
var width = 500,
height = 500,
padding = 1.5, // separation between same-color circles
clusterPadding = 4, // separation between different-color circles
maxRadius = 40;
var n = 5, // total number of circles
m = 1; // 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 = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
r = maxRadius,
d = {cluster: i, radius: r};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
return d;
});
// Use the pack layout to initialize node positions.
d3.layout.pack()
.sort(null)
.size([width, height])
.children(function(d) { return d.values; })
.value(function(d) { return d.radius * d.radius; })
.nodes({values: d3.nest()
.key(function(d) { return d.cluster; })
.entries(nodes)});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("body").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); })
.on("click", function(d) {
d.radius *= 1.1;
d3.select(this).attr("r", d.radius);
})
.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;
});
};
}
That behaviour you described happens because the force simulation has ended. So, the simplest solution is reheating the force at every click:
.on("click", function(d) {
d.radius *= 1.1;
d3.select(this).attr("r", d.radius);
force.resume();
})
Here, force.resume():
Sets the cooling parameter alpha to 0.1. This method sets the internal alpha parameter to 0.1, and then restarts the timer. Typically, you don't need to call this method directly; it is called automatically by start. It is also called automatically by drag during a drag gesture.
Here is your updated fiddle: https://jsfiddle.net/usb7nhfm/

Adding text to circle nodes d3

I am trying to add text to some circle nodes but I am having trouble doing so.
Here is the code for the main file. I am trying to create a second set of nodes called namenodes that contain text and then I am trying to attach namenodes to the original nodes that contain the circles. I'm not sure if this is the correct approach for this problem. When ever I run the code below, a bunch of black circles appear in the top left corner. I don't care if all of the nodes say the same thing but I would at least like to have some text appear. I think that the problem lies within the line
var namenodes = d3.range(200).map(function() { return {name: "hello"};}),
but I am not sure. It compiles without errors but it's not doing what I want. Any insight would be appreciated.
var nodes = d3.range(200).map(function() {
return {
radius: Math.random() * 12 + 4
};
}),
root = nodes[0],
color = d3.scale.category10();
//my code
var namenodes = d3.range(200).map(function() { return {name: "hello"};}),
nameroot = namenodes[0],
color = "black";
root.radius = 0;
root.fixed = true;
nameroot.radius = 0;
nameroot.fixed = true;
var force = d3.layout.force()
.gravity(0.05)
.charge(function(d, i) {
return i ? 0 : -2000;
})
.nodes(nodes)
.size([width, height]);
// var force = d3.layout.force()
//.gravity(0.05)
//.charge(function(d, i) {
//.nodes(namenodes)
//.size([width, height]);
force.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.selectAll("circle")
.data(nodes.slice(1))
.enter().append("circle")
.attr("r", function(d) {
return d.radius;
})
.style("fill", function(d, i) {
return color(i % 50);
});
//my code
//svg.selectAll("names")
//.data(namenodes.slice(1))
//.enter().append("names")
//.style(color);
force.on("tick", function(e) {
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) q.visit(collide(nodes[i]));
svg.selectAll("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
});
svg.on("mousemove", function() {
var p1 = d3.mouse(this);
root.px = p1[0];
root.py = p1[1];
//force.resume();
});
svg.on("click", function() {
if (force.alpha()) {
force.stop();
} else {
force.resume();
}
});
svg.selectAll("circle").on("click", function() {
d3.event.stopPropagation();
this.remove();
});
function collide(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * .5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
</script>

Draw pie chart with different radius in D3 (combine force network with pie chart)

I am trying to do combine pie chart with force network in D3. I currently have the force network working (shown below). I want to turn each bubble into a pie chart. Is that possible since they have different radius. Any general methods would be appreciated. Thanks!
This is the current javascript code:
d3.json("data/d3data.json", function(error, graph) {
var new_nodes = convert(graph.nodes);
force
.nodes(new_nodes)
// .links(graph.links)
.start();
var root = new_nodes[0];
root.fixed = true;
var loading = svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Simulating. One moment pleaseā€¦");
var node = svg.selectAll("svg")
.data(new_nodes)
.enter().append("svg")
.attr("width", function(d) { return Math.sqrt(d.followersCount/100)*2;})
.attr("height", function(d) { return Math.sqrt(d.followersCount/100)*2;});
var g = node.append("g")
.attr("transform", function(d) { return "translate(" + d.r + "," + d.r + ")"});
var g_2 = node.selectAll("g")
.data(function(d) {
console.log(pie(d.FFratio));
return pie(d.FFratio);})
.enter() .append("g");
g_2.append("path")
.attr("d", d3.svg.arc().outerRadius(10))
.style("fill", function (d, i) {
return color(i);
})
function tick(){
var node_x = 0;
var node_y = 0;
node.attr("x", function(d) { node_x = d.x; return d.x; })
.attr("y", function(d) { node_y = d.y;return d.y; });
}
loading.remove();
force.on("tick", function ticky(e){
var q = d3.geom.quadtree(graph.nodes),
i = 0,
n = graph.nodes.length;
while (++i < n) q.visit(collide(graph.nodes[i]));
tick();
});
function convert(node_list){
var result = [];
var current_node = {};
count = 0;
while (++count<node_list.length){
current_node = node_list[count];
current_node.r = Math.sqrt(current_node.followersCount/100);
var followingR = current_node.followingCount/(current_node.followersCount+current_node.followingCount)*100;
var followerR = 1 - followingR;
current_node.FFratio = [followingR, followerR];
result.push(current_node);
};
return result;
};
function collide(node) {
var node_r = Math.sqrt(node.followersCount/100),
r = node_r + 100,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node_r + Math.sqrt(quad.point.followersCount/100);
if (l < r) {
l = (l - r) / l * .5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
};
});
Here's an example I just coded up based off a clustered force layout.
The key is to replace the force clustered circle with a g that you can then loop and build pie charts inside of:
var pies = svg.selectAll(".pie")
.data(nodes)
.enter().append("g")
.attr("class","pie")
.call(force.drag);
pies.each(function(d,i){
var pieG = d3.select(this);
var arc = d3.svg.arc()
.outerRadius(d.radius)
.innerRadius(0);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return d;
});
var data = [Math.random(), Math.random(), Math.random(), Math.random()];
var g = pieG.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.attr("fill", function(d,i){
return colors(i);
})
});
Produces this:

D3 transition: Fading in and out the colors within a gradient fill

In this D3 diagram, the circles are filled with radial gradients, and changing opacity is used for fading in and fading out:
var width = 400,
height = 400,
padding = 1.5, // separation between same-color nodes
clusterPadding = 6, // separation between different-color nodes
maxRadius = 12;
var n = 200, // total number of nodes
m = 10; // 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 = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {cluster: i, radius: r};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
return d;
});
// Use the pack layout to initialize node positions.
d3.layout.pack()
.sort(null)
.size([width, height])
.children(function(d) { return d.values; })
.value(function(d) { return d.radius * d.radius; })
.nodes({values: d3.nest()
.key(function(d) { return d.cluster; })
.entries(nodes)
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var grads = svg.append("defs").selectAll("radialGradient")
.data(nodes)
.enter()
.append("radialGradient")
.attr("gradientUnits", "objectBoundingBox")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", "100%")
.attr("id", function(d, i) { return "grad" + i; });
grads.append("stop")
.attr("offset", "0%")
.style("stop-color", "white");
grads.append("stop")
.attr("offset", "100%")
.style("stop-color", function(d) { return color(d.cluster); });
var node = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.style("fill", function(d, i) {
return "url(#grad" + i + ")";
})
// .style("fill", function(d) { return color(d.cluster); })
.call(force.drag)
.on("mouseover", fade(.1))
.on("mouseout", fade(1));
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 fade(opacity) {
return function(d) {
node.transition().duration(1000)
.style("fill-opacity", function(o) {
return isSameCluster(d, o) ? 1 : opacity;
})
.style("stroke-opacity", function(o) {
return isSameCluster(d, o) ? 1 : opacity;
});
};
};
function isSameCluster(a, b) {
return a.cluster == b.cluster;
};
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;
});
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
(Same code as a jsfiddle)
How to use color for fading in and fading out, instead of opacity? For example, let say we want to make all circles gray while in "faded out" state", and bring them back to their original color in their "normal state"? You can't just transition the fill property as a color value, because the fill is a URL reference to a <radialGradient> element.
If you were using solid color fills, it would be straightforward to transition them to gray and then back to color -- just use the d3 transition of the fill property instead of the fill-opacity and stroke-opacity properties.
However, the colors in this case aren't actually associated with the elements in your selection. Instead, they are specified within the <stop> elements of the <radialGradient> created for each category. (Actually, they are currently created for each individual circle -- see my note below.) Therefore, you need to select these elements to transition the stop colors.
Because you're transitioning all elements in a given category at the same time, you wouldn't need to create additional gradient elements -- you just need a way to select the gradients associated with those categories, and transition them.
Here's your original code for creating the gradient elements, and referencing them to color the circles:
var grads = svg.append("defs").selectAll("radialGradient")
.data(nodes)
.enter()
.append("radialGradient")
.attr("gradientUnits", "objectBoundingBox")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", "100%")
.attr("id", function(d, i) { return "grad" + i; });
grads.append("stop")
.attr("offset", "0%")
.style("stop-color", "white");
grads.append("stop")
.attr("offset", "100%")
.style("stop-color", function(d) { return color(d.cluster); });
var node = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.style("fill", function(d, i) {
return "url(#grad" + i + ")";
})
.call(force.drag)
.on("mouseover", fade(.1))
.on("mouseout", fade(1));
The fade() function you're currently using generates a separate event handler function for each element, which will then select all the circles and transition them to the specified opacity, or to full opacity, according to whether they are in the same cluster as the circle that received the event:
function fade(opacity) {
return function(d) {
node.transition().duration(1000)
.style("fill-opacity", function(o) {
return isSameCluster(d, o) ? 1 : opacity;
})
.style("stroke-opacity", function(o) {
return isSameCluster(d, o) ? 1 : opacity;
});
};
};
function isSameCluster(a, b) {
return a.cluster == b.cluster;
};
To transition the gradients instead, you need to select the gradients instead of the circles, and check which cluster they are associated with. Since the gradient elements are attached to the same data objects as the nodes, you can reuse the isSameCluster() method. You just need to change the inner function within the fade() method:
function fade(saturation) {
return function(d) {
grads.transition().duration(1000)
.select("stop:last-child") //select the second (colored) stop
.style("stop-color", function(o) {
var c = color(o.cluster);
var hsl = d3.hsl(c);
return isSameCluster(d, o) ?
c :
d3.hsl(hsl.h, hsl.s*saturation, hsl.l);
});
};
};
Some notes:
In order to select the correct stop element within the gradient, I'm using the :last-child pseudoclass. You could also just give the stop elements a normal CSS class when you create them.
To desaturate the color by the specified amount, I'm using d3's color functions to convert the color to a HSL (hue-saturation-luminance) value, and then multiply the saturation property. I multiply it, instead of setting it directly, in case any of your starting colors aren't 100% saturated. However, I would recommend using similarly saturated colors to get a consistent effect.
In the working example, I also changed your color palette so that you wouldn't have any gray colors to start out with (for the first 10 clusters, anyway). You'll probably need to create a custom palette with similar saturation values for all colors.
If you want the final value for the fade-out effect to always be an identical gray gradient, you could probably simplify the code quite a bit -- remove all the hsl calculations, and use a boolean parameter instead of a numerical saturation value. Or even just have two functions, one that resets all the colors, without needing to test for which cluster is which, and one that tests for clusters and sets values to gray accordingly.
Working snippet:
var width = 400,
height = 400,
padding = 1.5, // separation between same-color nodes
clusterPadding = 6, // separation between different-color nodes
maxRadius = 12;
var n = 200, // total number of nodes
m = 10; // number of distinct clusters
var color = d3.scale.category20()
.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 = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {cluster: i, radius: r};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
return d;
});
// Use the pack layout to initialize node positions.
d3.layout.pack()
.sort(null)
.size([width, height])
.children(function(d) { return d.values; })
.value(function(d) { return d.radius * d.radius; })
.nodes({values: d3.nest()
.key(function(d) { return d.cluster; })
.entries(nodes)
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var grads = svg.append("defs").selectAll("radialGradient")
.data(nodes)
.enter()
.append("radialGradient")
.attr("gradientUnits", "objectBoundingBox")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", "100%")
.attr("id", function(d, i) { return "grad" + i; });
grads.append("stop")
.attr("offset", "0%")
.style("stop-color", "white");
grads.append("stop")
.attr("offset", "100%")
.style("stop-color", function(d) { return color(d.cluster); });
var node = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.style("fill", function(d, i) {
return "url(#grad" + i + ")";
})
// .style("fill", function(d) { return color(d.cluster); })
.call(force.drag)
.on("mouseover", fade(.1))
.on("mouseout", fade(1));
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 fade(saturation) {
return function(d) {
grads.transition().duration(1000)
.select("stop:last-child") //select the second (colored) stop
.style("stop-color", function(o) {
var c = color(o.cluster);
var hsl = d3.hsl(c);
return isSameCluster(d, o) ?
c :
d3.hsl(hsl.h, hsl.s*saturation, hsl.l);
});
};
};
function isSameCluster(a, b) {
return a.cluster == b.cluster;
};
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;
});
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Note:
Currently, you're creating a separate <radialGradient> for each circle, when you really only need one gradient per cluster. You could improve the overall performance by using your clusters array as the data for the gradient selection instead of your nodes array. However, you would need to then change the id values for the gradients to be based on the cluster data rather than on the index of the node.
Using filters, as suggested by Robert Longson in the comments, would be another option. However, if you wanted a transition effect, you would still need to select the filter elements and transition their attributes. At least for now. When CSS filter functions are more widely supported, you would be able to directly transition a filter: grayscale(0) to filter: grayscale(1).

D3: How to assign size, colour to items (collision-detection visualisation)

I'm new to D3 and have built the following visualisation based on the collision-detection visualisation example on the D3js.io website (http://bl.ocks.org/mbostock/3231298).
I'd like to be able to specify the size and colour of each of the 10 balls in my graph but I don't understand how to do that. At the moment, the code is relying on randomly generated size and the balls are all black. Can someone please explain how I can make these changes?
Ideally, I'd like to be able to specify hex codes and a specific px-width for each of the 10 balls.
My code is pasted below, and is also on Codepen: http://codepen.io/msummers40/pen/LiBmr
Thank you, in advance, for your assistance.
Matt
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 500,
height = 400;
var nodes = d3.range(11).map(function() { return {radius: Math.random() * 33 + 4}; }),
root = nodes[0],
color = d3.scale.category20();
root.radius = 80;
root.fixed = true;
var force = d3.layout.force()
.gravity(0.05)
.charge(function(d, i) { return i ? 0 : -2000; })
.nodes(nodes)
.size([width, height]);
force.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.selectAll("circle")
.data(nodes.slice(1))
.enter().append("circle")
.attr("r", function(d) { return d.radius; })
.style("fill", function(d) { return d.color; });
force.on("tick", function(e) {
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) q.visit(collide(nodes[i]));
svg.selectAll("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
svg.on("mousemove", function() {
var p1 = d3.mouse(this);
root.px = p1[0];
root.py = p1[1];
force.resume();
});
function collide(node) {
var r = node.radius + 1,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * .5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
</script>
</html>
The radius and fill colour for the circles are set in this code (specifically the last two lines):
svg.selectAll("circle")
.data(nodes.slice(1))
.enter().append("circle")
.attr("r", function(d) { return d.radius; })
.style("fill", function(d, i) { return color(i % 3); });
The radius comes directly from the data, where it is generated like this:
{radius: Math.random() * 33 + 4}
To set a specific radius per circle, change what is returned there. If you want to specify explicitly the radii, have some static data like this:
var nodes = [{radius: 1}, {radius: 2}, ...];
The same works for the fill colour. You could add another attribute to the data to specify the colour (it looks like you're trying to do this already):
var nodes = [{radius: 1, color: "red"}, {radius: 2, color: "blue"}, ...];
You almost have it, but the style method is not ok. You should change to attr('fill'), and then use your color scale:
svg.selectAll("circle")
.data(nodes.slice(1))
.enter().append("circle")
.attr("r", function(d) { return d.radius; })
.attr("fill", function(d) { return color(d.index); });
And you can do exaclty the same for size or whatever

Categories

Resources