Skip to content

Instantly share code, notes, and snippets.

@gmamaladze
Forked from mbostock/.block
Last active February 4, 2021 21:53
Show Gist options
  • Save gmamaladze/9320969 to your computer and use it in GitHub Desktop.
Save gmamaladze/9320969 to your computer and use it in GitHub Desktop.
Nodes snapping to colored clusters - d3.js sample

Drag gray nodes into one of the colored clusters. They will snap to the group. You can also move nodes out of groups to regroup them.

Created using D3's force layout. Forekd from Multi-Foci Force Layout.

This code is distributed under MIT license. Copyright (c) 2013 George Mamaladze. See MIT license.

<!DOCTYPE html>
<html>
<head>
<title>Nodes snapping to colored clusters</title>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<div id="chart"></div>
<script type="text/javascript">
var w = 960,
h = 500,
count = 30,
fill = d3.scale.category10();
nodes = d3.range(count).map(Object);
var groupPath = function (d) {
return "M" +
d3.geom.hull(d.map(function (e) { return [e.x, e.y]; }))
.join("L")
+ "Z";
};
var groups = [];
groups.init = function () {
groups.a = Math.min(w, h);
groups.r = groups.a * 0.20;
for (i = 0; i < 4; i++) {
var g = [];
g.groupId = i;
g.x = w / 2;
g.y = h / 2;
var k = 0.25 * groups.a;
g.x += i & 2 ? k : -k;
g.y += i & 1 ? k : -k;
for (j = 0; j < 5; j++) {
var alpha = (2 * Math.PI * j / 5); 50
var dummy = { x: g.x + 10 * Math.sin(alpha), y: g.y + 10 * Math.cos(alpha) };
g.push(dummy);
}
groups[i] = g;
};
}
groups.snap = function (d) {
function change(d, to) {
if (d.groupId != null) {
var oldGroup = groups[d.groupId];
var index = oldGroup.indexOf(d);
if (index >= 0) {
oldGroup.splice(index, 1);
}
}
if (to == null) {
d.groupId = null;
} else {
d.groupId = to.groupId;
to.push(d);
}
}
groups.forEach(function (g) {
var distance = Math.sqrt(Math.pow(d.x - g.x, 2) + Math.pow(d.y - g.y, 2));
if (distance < groups.r) {
if (d.groupId != g.groupId) {
change(d, g);
}
} else {
if (d.groupId == g.groupId) {
change(d, null)
}
}
});
}
groups.delta = function (d) {
function massCenter(g) {
var x = 0, y = 0;
g.forEach(function (e) {
x += e.x;
y += e.y;
});
return { x: x / g.length, y: y / g.length };
}
if (d.groupId == null) return 0;
var g = groups[d.groupId];
var massCenter = massCenter(g);
var delta = {
x: g.x - massCenter.x,
y: g.y - massCenter.y
}
return delta;
}
groups.init();
var nodeFill = function (n) { return n.groupId != null ? fill(n.groupId) : "lightgray"; };
var vis = d3.select("#chart").append("svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.nodes(nodes)
.links([])
.size([w, h])
.start();
var node = vis.selectAll("circle.node")
.data(nodes)
.enter().append("circle")
.attr("class", "node")
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("r", 8)
.style("fill", nodeFill)
.style("stroke", function (d, i) { return d3.rgb(nodeFill).darker(2); })
.style("stroke-width", 1.5)
.on("mousemove", groups.snap)
.call(force.drag);
vis.style("opacity", 1e-6)
.transition()
.duration(1000)
.style("opacity", 1);
var group = vis.selectAll("path")
.data(groups)
.attr("d", groupPath)
.enter().insert("path", "circle")
.style("fill", nodeFill)
.style("stroke", nodeFill)
.style("stroke-width", 40)
.style("stroke-linejoin", "round")
.style("opacity", .2)
force.on("tick", function (e) {
nodes.forEach(function (o) {
if (o.groupId == null) return;
o.x += groups.delta(o).x * .3;
o.y += groups.delta(o).y * .3;
});
node.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.style("fill", nodeFill);
group.attr("d", groupPath);
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment