Skip to content

Instantly share code, notes, and snippets.

@Andrew-Reid
Last active August 23, 2018 21:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Andrew-Reid/6edf5fea96a6987ee53b8683c42aa50b to your computer and use it in GitHub Desktop.
Save Andrew-Reid/6edf5fea96a6987ee53b8683c42aa50b to your computer and use it in GitHub Desktop.
D3-fuse

This is a demonstration of d3-fuse, a clustering library that allows the visual fusion of nodes that are overlapping.

The branching tree on each node cluster shows the nodes that comprise that cluster based on node overlap. Due to a moving center of gravity between each step of the clustering process and radii of the clustered nodes, trees can extend slightly beyond the node disc.

This is an initial attempt at a clustering library that doesn't rely on d3-force, though the short code of d3-fuse is borrows considerably from d3.forceCollide.

In this demonstration the mouse wheel will simulate a zoom by increasing/decreasing circle radii.

/*v0.0.0*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-quadtree')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-quadtree'], factory) :
(factory((global.d3 = global.d3 || {}),global.d3)); }(this, (function (exports,d3Quadtree) { 'use strict';
var c = function(f) { return (typeof f == "function") ? f : (function() { return f; }) }
var fuse = function(n) {
var nodes = n || [], padding = 0, pi = Math.PI;
var x = function(d) { return d.x; },
y = function(d) { return d.y; },
r = function(d) { return d.r; },
a = function(d) { return r(d) * r(d) * Math.PI; }
function fuse() { initializeNodes(), step(); return nodes; }
function cluster() {
var tree = d3Quadtree.quadtree(nodes, function(d) { return d.layout.x; }, function(d) { return d.layout.y; }).visitAfter(prepare);
var n0; // Current Node, n1 = comparison node.
var count = 0; // Number of merges for a given cycle
for (var i = 0; i < nodes.length; ++i) n0 = nodes[i], tree.visit(apply);
function apply(qn, x0, y0, x1, y1) {
var n1 = qn.data;
var r = qn.r + n0.layout.r;
if (n1 && n1.index > n0.index && n1.layout.a && n0.layout.a) {
var x = n0.layout.x - n1.layout.x || 1e-6;
var y = n0.layout.y - n1.layout.y || 1e-6;
var l = Math.sqrt(x * x + y * y);
if (l < r + padding) { // If merge required
l = (r - l) / l;
count++;
// Merge logic
var a,b;
if(n1.layout.a > n0.layout.a) a = n1, b = n0; // Node1 absorbs Node0
else a = n0, b = n1; // Node0 absorbs Node1
// Merge nodes:
a.layout.x = (a.layout.x * a.layout.a + b.layout.x * b.layout.a)/(a.layout.a + b.layout.a);
a.layout.y = (a.layout.y * a.layout.a + b.layout.y * b.layout.a)/(b.layout.a + a.layout.a);
a.layout.count += b.layout.count;
a.layout.a += b.layout.a;
a.layout.r = Math.sqrt(a.layout.a/pi);
b.layout.r = b.layout.a = 0;
a.layout.children.push(b), b.layout.parent = a;
}
return;
}
return x0 > n0.layout.x + r || x1 < n0.layout.x - r || y0 > n0.layout.y + r || y1 < n0.layout.y - r;
}
return count;
}
function prepare(n) {
if (n.data) return n.r = n.data.layout.r;
for (var i = n.r = 0; i < 4; ++i) {
if (n[i] && n[i].r > n.r) {
n.r = n[i].r;
}
}
}
function step() { if(cluster()) step(); }
function initializeNodes() {
for (var i = 0, n = nodes.length, node; i < n; ++i) {
node = nodes[i], node.index = i;
node.layout = { x:x(node), y:y(node), a: a(node), r: r(node), count: 1, children: [], parent: {} }
}
}
fuse.nodes = function(_) { return arguments.length ? (nodes = _, fuse) : nodes; }
fuse.padding = function(_) { return arguments.length ? (padding = _, fuse) : padding; }
fuse.radius = function(_) { return arguments.length ? (radius = c(_), fuse) : radius; }
fuse.area = function(_) { return arguments.length ? (area = c(_), fuse) : area; }
fuse.x = function(_) { x = c(_); return fuse; }
fuse.y = function(_) { y = c(_); return fuse; }
fuse.defuse = function() { nodes.forEach(function(n) { delete n.layout; }); return fuse; }
fuse.step = function() { initializeNodes(); cluster(); return fuse; }
return fuse;
};
exports.fuse = fuse;
Object.defineProperty(exports, '__esModule', { value: true });
})));
<!DOCTYPE html>
<meta charset="utf-8">
<canvas width="960" height="600"></canvas>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="d3-fuse.js"></script>
<script>
var canvas = d3.select("canvas"),
ctx = canvas.node().getContext("2d"),
width = +canvas.attr("width"),
height = +canvas.attr("height");
// Create nodes:
var nodes = d3.range(10000).map(function() {
return {
r: 3.25,
a: Math.PI,
x: Math.random()*width,
y: Math.random()*height
}
})
// Set up cluster
var cluster = d3.fuse()
.nodes(nodes);
draw(cluster());
// Modify circle radius with zoom scale:
var zoom = d3.zoom()
.on("zoom", function() {
var k = Math.pow(d3.event.transform.k, 0.3);
// Modify nodes' radii/area (simulated zoom):
nodes.forEach(function(node) {
node.r = 3.25 * k;
node.a = Math.PI * node.r * node.r;
})
draw(cluster());
})
canvas.call(zoom);
// Drawing functions:
function draw(nodes) {
ctx.clearRect(0,0,width,height);
nodes.filter(function(d) { return d.layout.r != 0; }).forEach(drawCircle);
nodes.forEach(drawLink);
nodes.filter(function(d) { return d.layout.r > 20; }).forEach(drawText);
}
function drawLink(d) {
d = d.layout;
if(d.parent.x && d.parent.y) {
ctx.beginPath()
ctx.strokeStyle = "#ccc";
ctx.moveTo(d.x,d.y)
ctx.lineTo(d.parent.layout.x,d.parent.layout.y)
ctx.stroke();
}
}
function drawCircle(d) {
d = d.layout;
ctx.fillStyle = d.r ? ( d.r > 20 ? "#a8ddb5" : "#43a2ca" ) : "#0868ac";
ctx.beginPath();
ctx.moveTo(d.x, d.y);
ctx.arc(d.x, d.y, d.r, 0, 2 * Math.PI);
ctx.fill();
}
function drawText(d) {
d = d.layout;
ctx.font = d.r / 3 + "px Arial";
ctx.textAlign = "center";
ctx.fillStyle = "white";
ctx.fillText(d.count,d.x,d.y+d.r/9);
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment