thanks for reading -- I am designing an interactive foreign language teaching tool using the D3js force bubble layout. In essence, users can select a language textbook chapter, part of speech, or specific word and the interface will display the word(s) in a series of bubbles each clustered together by their part of speech (adjective, noun, verb, etc.). These bubbles are free floating, no links between them.
When you click a word I have it open up more bubbles with information about that specific word (lexeme, meaning in english, frequency, etc.), but I want those bubbles to be attached to the original bubble with links. I'm having a lot of trouble conceptualizing how to do this dynamically (source -> target), even though it should be pretty straightforward.
First I draw the bubbles based on user input, some of which I've left out so the code doesn't get too long:
var nodes = d3.range(n).map(function(j) { return createNode(colorList, j); });
function createNode(info, j){
var keys = objectKeys;
var cluster, r;
if (info[j].Lexeme.length < 3) {
var radius = info[j].Lexeme.length * 15;
}
else {
var radius = info[j].Lexeme.length * 5;
}
$.each(keys, function(x, y){
if (info[j].POS == x){
cluster = y;
}
});
d = {cluster: cluster, radius: radius, info: info[j]};
if (!clusters[cluster] || (r > clusters[cluster].radius)) clusters[cluster] = d;
return d;
}
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(.00001)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("#canvas").append("svg")
.attr("width", width)
.attr("height", height);
var node = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function (d, i) { return "node_n" + i; })
.style("fill", function(d) {return d.info.fillColor;})
.on("click", nodeSelect)
.on("dblclick", infoNode)
.call(force.drag);
//Add the SVG Text Element to the svgContainer
var text = svg.selectAll("text")
.data(nodes)
.enter()
.append("text");
//Add SVG Text Element Attributes
text
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.text( function (d) { return d.info.Lexeme; })
.attr("font-family", "sans-serif")
.attr("text-anchor", "middle")
.attr("font-size", "13px")
.attr("fill", "#000000");
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(5 * e.alpha * e.alpha))
.each(collide(.3))
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
text
.attr("transform", transform);
}
function transform(d) {
return "translate(" + d.x + "," + d.y + ")";
}
function nodeSelect(d) {
if (d3.select(this).style("opacity") == 1 && nodeSelected == 0) {
nodeSelected = 1;
d3.select(this).style("opacity", ".3");
}
else if (d3.select(this).style("opacity") == .3 && nodeSelected == 1){
nodeSelected = 0;
d3.select(this).style("opacity", "1");
}
}
// 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;
});
};
}
function infoNodes(d){
$.each(d.info, function(i, val){
if (val != "" && val !="NA" && i != "fillColor"){
var b = d.cluster;
var r = 50;
var q = {cluster: b, radius: r, info: val, fillColor: d.info.fillColor};
if (!clusters[cluster] || (r > clusters[cluster].radius)) clusters[cluster] = q;
nodes.push(q);
}
});
update(d);
}
function update(j) {
node = svg.selectAll("circle")
.data(nodes)
.call(force.drag);
node.enter().append("circle")
.attr("class", function (d, i) { return "info_" + d;})
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("r", function (d) { if (d.info.length < 5 || d.info.length == null) { return 30; } else { return d.info.length * 3.3; } })
.style("fill", function(d) {return d.fillColor;})
.call(force.drag);
text = svg.selectAll("text")
.data(nodes)
.call(force.drag);
text.enter().append("text")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.text( function (d) { return d.info; })
.attr("font-family", "sans-serif")
.attr("text-anchor", "middle")
.attr("font-size", "13px")
.attr("fill", "#000000")
.call(force.drag);
force.start();
}
The function can draw info nodes and update, but I can't figure out how to draw info nodes attached to the node you click with lines that connect them.
Let me know if more explanation is needed.
Related
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:
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).
How to draw a circle with gradient color? Say, a gradient from yellow to blue.
Normally, to create a circle in yellow we can use the following code:
var cdata=[50,40];
var xscale=40;
var xspace =50;
var yscale=70;
var svg = d3.select("body")
.append("svg")
.attr("width", 1600)
.attr("height", 1600);
var circle = svg.selectAll("circle")
.data(cdata)
.enter()
.append("circle");
var circleattr = circle
.attr("cx", function(d) {
xscale = xscale+xspace;
return xscale;
})
.attr("cy", function(d) {
yscale=yscale+xspace+10;
return yscale;
})
.attr("r", function(d) {
return d;
})
.style("fill","yellow");
You have to define the gradient in the SVG first, and then fill the circle with an SVG link to the gradient element.
// Define the gradient
var gradient = svg.append("svg:defs")
.append("svg:linearGradient")
.attr("id", "gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "100%")
.attr("spreadMethod", "pad");
// Define the gradient colors
gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", "#a00000")
.attr("stop-opacity", 1);
gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", "#aaaa00")
.attr("stop-opacity", 1);
// Fill the circle with the gradient
var circle = svg.append('circle')
.attr('cx', width / 2)
.attr('cy', height / 2)
.attr('r', height / 3)
.attr('fill', 'url(#gradient)');
A jsFiddle with the complete example. More details on how to define SVG gradients in the MDN Tutorial. The resulting image:
Take a look at this code snippet:
var width = 500,
height = 500,
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);
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;
});
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
It contains support for drawing SVG circles with gradient (and achieving a 3D look-and-feel effect by doing this) and is based on SVG radial gradients.
For each node, a gradient is defined:
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); });
Then, instead of line:
.style("fill", function(d) { return color(d.cluster); })
this line is added in the code that creates circles:
.attr("fill", function(d, i) {
return "url(#grad" + i + ")";
})
This produces this effect:(animated gif that I used has some limitations for number of colors, so gradients are not smooth as in real example)
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.
I'm trying to create an animated bubble chart using d3js. It seems to be working except for one small thing: when i animate the size of the nodes they start to overlap.
My co-worker and me have been at it all morning but we seem to be missing or overlooking something.
Our html/js:
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
</style>
<body>
<div class="buttons">
<button id="twelve">2012</button>
</div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script>
var width = 960,
height = 500;
var force = d3.layout.force()
.charge(0.5)
.gravity(0.2)
.distance(500)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("miserables.json", function(error, graph) {
force
.nodes(graph.nodes)
.distance( function(d){ return d.radius})
.start();
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", function(d) { return d.radius; })
.style("fill", function(d) { return d.color; })
.call(force.drag);
node.append("title")
.text(function(d) { return d.name; });
force.on("tick", function(e) {
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
var nodes = graph.nodes
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; })
});
$(document).ready(function(){
$('.buttons button').on('click', function(){
var showYear = $(this).attr('id');
node.transition()
.duration(350)
.attr("r", function(d){ return d[showYear]["radius"]})
.style("fill", function(d){ return d[showYear]["color"]})
});
})
});
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>
</body>
and this is our json file:
{
"nodes":[
{
"name": "Country One",
"radius": 40,
"color": "#ff0000",
"twelve" : {
"radius" : 60,
"color" : "#0ff000"
}
},
{
"name": "Country Two",
"radius": 40,
"color": "#ffff00",
"twelve" : {
"radius" : 60,
"color" : "#0ff000"
}
}
]
}
Any help would be greatly appreciated.
The collision detection is done based on the radius attribute of the nodes, but I suppose you're animating only the radius of your SVG circles, not the radius attribute of the data behind them.
Proposed solution:
node
.each( function( d ) {
d.radius = d[showYear]["radius"];
})
.transition()
.duration(350)
.attr("r", function(d){ return d.radius})
.style("fill", function(d){ return d[showYear]["color"]})
What's also a bit odd in my opinion, but may be fine: At creation time there is a d.radius:
.attr("r", function(d) { return d.radius; })
but later it becomes d[showYear]["radius"];. Keep in mind that the d object holds only data for one node, not all of them.