|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
|
|
|
|
<script src="http://d3js.org/d3.v3.min.js"></script> |
|
|
|
<body> |
|
Drag the bubbles using your mouse (or finger). |
|
</body> |
|
|
|
<script> |
|
|
|
var width = 960, |
|
height = 500, |
|
padding = 10, // separation between same-color circles |
|
maxRadius = 5, |
|
clusterPadding = 25; // separation between different-color circles |
|
|
|
var n =50, // total number of circles |
|
m = 5; // number of distinct clusters |
|
|
|
|
|
//container |
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width) |
|
.attr("height", height); |
|
var allChartBg_vorinoi = svg.append("g"); |
|
var allChartBg_group = svg.append("g"); |
|
//******************************************************* |
|
|
|
|
|
|
|
|
|
//******************************************************* |
|
//function variables |
|
//******************************************************* |
|
//color generator |
|
var color = d3.scale.category10().domain(d3.range(m)); |
|
|
|
//custom drag |
|
var drag = d3.behavior.drag() |
|
.origin(function(d) { return d; }) |
|
.on("dragstart", dragstarted) |
|
.on("drag", dragged) |
|
.on("dragend", dragended); |
|
|
|
|
|
//path generator |
|
var groupFunction = d3.svg.line() |
|
.interpolate("basis-closed") |
|
.tension(0.0) |
|
.x(function(d) { return d[0]; }) |
|
.y(function(d) { return d[1]; }); |
|
//******************************************************* |
|
|
|
|
|
|
|
|
|
//******************************************************* |
|
//node generator |
|
//******************************************************* |
|
// 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; |
|
}); |
|
//******************************************************* |
|
|
|
|
|
|
|
|
|
//******************************************************* |
|
//elements |
|
//******************************************************* |
|
//hidden circles |
|
var circle = svg.selectAll("custom.circle").data(nodes); |
|
circle.enter().append("custom") |
|
.classed("circle", true) |
|
.attr("id", function(d, i) { return (clusters[d.cluster] === d) ? "cluster_" + d.cluster : "circle_" + i; }) |
|
.attr("r", function(d) { return d.radius; }); |
|
|
|
|
|
//grouping bubble |
|
var groupPath; |
|
var groups = d3.nest().key(function (d) { return d.cluster + ""; }).entries(nodes); |
|
groups.forEach(function(n) { |
|
n.strokeWidth = d3.max(n.values, function(d) { return d.radius; }); |
|
}); |
|
groupPath = allChartBg_group.selectAll(".groupPath").data(groups); |
|
groupPath.enter().append("path") |
|
.attr("class", "groupPath") |
|
.style("opacity", 1) |
|
.style("fill", function(d) { return color(d.key); }) |
|
.style("stroke", function(d) { return color(d.key); }) |
|
.style("stroke-linejoin", "round") |
|
.style("stroke-width", function(d) { return d.strokeWidth * 3; }) |
|
.on("mousedown", function(d) { |
|
selectedCluster = d.key; |
|
}) |
|
.call(drag); |
|
groupPath.exit().remove(); |
|
//******************************************************* |
|
|
|
|
|
|
|
|
|
//******************************************************* |
|
//animation |
|
//******************************************************* |
|
var force = d3.layout.force() |
|
.nodes(nodes) |
|
.size([width, height]) |
|
.gravity(0) |
|
.charge(0) |
|
.on("tick", tick) |
|
.start(); |
|
|
|
function tick(e) { |
|
circle |
|
.each(cluster(10 * e.alpha * e.alpha)) |
|
.each(collide(.25)) |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }); |
|
|
|
//grouping bubble |
|
groupPath.attr("d", function(d) { return groupFunction(d3.geom.hull(d.values.map(function (i) { return [i.x, i.y]; }))); }); |
|
} |
|
//******************************************************* |
|
|
|
|
|
|
|
//******************************************************* |
|
// clustering function |
|
//******************************************************* |
|
// Move d to be adjacent to the cluster node. |
|
function cluster(alpha) { |
|
return function(d) { |
|
var cluster = clusters[d.cluster], |
|
k = 1; |
|
|
|
// For cluster nodes, apply custom gravity. |
|
if (cluster === d) { |
|
cluster.x = (cluster.x === undefined) ? width /2 : cluster.x; |
|
cluster.y = (cluster.y === undefined) ? height /2 : cluster.y; |
|
cluster = {x: width / 2, y: height / 2, radius: -d.radius}; |
|
//cluster.radius = (cluster.radius === undefined) ? d.radius : cluster.radius; |
|
k = .1 * Math.sqrt(d.radius); |
|
} |
|
|
|
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 * k; |
|
d.x -= x *= l; |
|
d.y -= y *= l; |
|
cluster.x += x; |
|
cluster.y += y; |
|
} |
|
}; |
|
} |
|
//******************************************************* |
|
|
|
|
|
|
|
|
|
//******************************************************* |
|
// collision handler |
|
//******************************************************* |
|
// 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; |
|
}); |
|
}; |
|
} |
|
//******************************************************* |
|
|
|
|
|
|
|
|
|
//******************************************************* |
|
//custom drag functions |
|
//******************************************************* |
|
function dragstarted(d) { |
|
d3.select(this).classed("dragging", true); |
|
} |
|
function dragged(d) { |
|
d3.select("#cluster_" + selectedCluster) |
|
.attr("cx", function(c) { |
|
c.x = d3.event.sourceEvent.x; |
|
return d3.event.sourceEvent.x; |
|
}) |
|
.attr("cy", function(c) { |
|
c.y = d3.event.sourceEvent.y; |
|
return d3.event.sourceEvent.y; |
|
}); |
|
force.start(); |
|
} |
|
function dragended(d) { |
|
d3.select(this).classed("dragging", false); |
|
} |
|
//******************************************************* |
|
|
|
</script> |