Computing K-Means within a d3.forceSimulation loop.
Forked from mbostock's block: Collision Detection
| license: gpl-3.0 | |
| height: 960 |
Computing K-Means within a d3.forceSimulation loop.
Forked from mbostock's block: Collision Detection
| <!DOCTYPE html> | |
| <meta charset="utf-8"> | |
| <canvas width="960" height="960"></canvas> | |
| <script src="https://d3js.org/d3.v4.min.js"></script> | |
| <script> | |
| var canvas = document.querySelector("canvas"), | |
| context = canvas.getContext("2d"), | |
| width = canvas.width, | |
| height = canvas.height, | |
| tau = 2 * Math.PI; | |
| var nodes = d3.range(1000).map(function(i) { | |
| return { | |
| r: Math.random() * 14 + 4 | |
| }; | |
| }); | |
| var K = 20, | |
| centers = d3.range(K).map(i => [0,0]), | |
| colorscale = d3.scaleOrdinal(d3.schemeCategory20c); | |
| var simulation = d3.forceSimulation(nodes) | |
| .velocityDecay(0.2) | |
| .force("x", d3.forceX().strength(0.002)) | |
| .force("y", d3.forceY().strength(0.002)) | |
| .force('kmeans', function(){ | |
| // a central point to re-init empty groups | |
| var m = [d3.mean(centers.map(d => d[0] || 0)), | |
| d3.mean(centers.map(d => d[1] || 0))]; | |
| // the order is important: move the centers before re-making the groups | |
| // so that the centers follow the general movement and keep "their" | |
| // points, instead of having points pass through them | |
| // 1. move K-centers towards the barycenter of their group | |
| centers.forEach((c,i) => { | |
| c[0] = d3.mean(nodes.filter(d => d.group == i).map(d => d.x)) || m[0]; | |
| c[1] = d3.mean(nodes.filter(d => d.group == i).map(d => d.y)) || m[1]; | |
| }); | |
| // 2. group each point according to its closest K-center | |
| nodes.forEach(d => { | |
| d.group = d3.scan(centers.map(c => { | |
| var dx = d.x - c[0], | |
| dy = d.y - c[1]; | |
| return (dx*dx + dy*dy); | |
| })); | |
| }); | |
| } | |
| ) | |
| .force("collide", d3.forceCollide().radius(function(d) { return d.r + 0.5; }).iterations(2)) | |
| /* | |
| .force("circle", function(alpha){ | |
| nodes.forEach(d => { | |
| let k = Math.sqrt(d.y*d.y + d.x * d.x); | |
| d.vx += d.y/k * 5 * alpha; | |
| d.vy -= d.x/k * 5 * alpha; | |
| }) | |
| }) | |
| */ | |
| .on("tick", ticked); | |
| function ticked() { | |
| context.clearRect(0, 0, width, height); | |
| context.save(); | |
| context.translate(width / 2, height / 2); | |
| nodes.forEach(function(d) { | |
| context.beginPath(); | |
| context.moveTo(d.x + d.r, d.y); | |
| context.arc(d.x, d.y, d.r, 0, tau); | |
| context.fillStyle = colorscale(d.group); | |
| context.fill(); | |
| context.strokeStyle = "#333"; | |
| context.stroke(); | |
| }); | |
| context.restore(); | |
| } | |
| </script> | |