Skip to content

Instantly share code, notes, and snippets.

@larsvers
Last active December 22, 2016 08:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save larsvers/5f8ab586c29faa8c2d0e3d2e4b424759 to your computer and use it in GitHub Desktop.
Save larsvers/5f8ab586c29faa8c2d0e3d2e4b424759 to your computer and use it in GitHub Desktop.
Force split/unite playground
license: mit

..to play around with splitting and re-uniting nodes. This can surely happen in numbers as it's canvas but I'd stay below a total of 5k nodes.

The only forces to keep this aesthetic were:

  1. a low alpha of 0.3 (0.2 on split and unite)
  2. a low repulsion with d3.forceManyBody().strength(-5)
  3. a radius-appropriate colliding force with d3.forceCollide(2) (2 is the radius)
  4. and a relatively high position strength with d3.forceX(width/2).strength(.7) (ame for .forceY)

Box-bounded toggles the constraint for the nodes to stay canvas-bound. Really simple 2 lines of code in the ticked() function:

    d.x = Math.max(d.radius, Math.min(width - d.radius, d.x));
    d.y = Math.max(d.radius, Math.min(height - d.radius, d.y));	

It takes either the current value of d.x or (if that's higher than the width minus the radius) the edge of the canvas. If this is smaller than the radius (in keeping this would be 0 + radius) it shall take that minimum position on the left. Whatever it takes, it overwrites the current value of d.x. Same for d.y.

Mike Bostock suggests to use forces to keep simulations within bounds, but sometimes you need to force them, I guess.

Built with blockbuilder.org

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>winter force</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="http://d3js.org/d3.v4.js"></script>
<style type="text/css">
/* === Meyer Reset & border box (in moderation) === */
html, body {
margin: 0;
padding: 0;
border: 0;
font-family: 'Open Sans', sans-serif;
font-size: 100%;
vertical-align: baseline;
height: 100%;
box-sizing: border-box;
}
*, *:before, *:after{
box-sizing: inherit;
}
a:link, a:visited, a:hover, a:active {
color: #000;
}
/* === Canvas === */
canvas {
border: 1px solid #ccc;
margin: 5px 50px;
}
h3, input#cluster1, button#split {
margin-left: 50px;
}
button {
margin: 0;
padding: .5em;
outline: none;
font-size: .65em;
color: #555;
background-color: rgba(255, 255, 255, .7);
border: none;
transition: all 0.1s ease;
}
button:hover {
background-color: rgba(204,204,204, .7);
cursor: pointer;
}
</style>
</head>
<body>
<h3>Force split and unite</h3>
<input type="text" id="cluster1" value="1500">
<input type="text" id="cluster2" value="750">
<button id="submit">Submit</button>
<div id="container"></div>
<button id="split">Split</button>
<button id="unite">Unite</button>
<button id="bounded">Box-bound</button>
<script>
// === Globals === //
var data;
var width = 500, height = 250;
var simulation, bounded = true;
// === Set up canvas === //
var canvas = d3.select('#container').append('canvas').attr('width', width).attr('height', height);
var context = canvas.node().getContext('2d');
// === First call === //
getSimulationData(1500, 750);
// === Get simulation data === //
function getSimulationData(cluster1, cluster2) {
var nodes = [];
d3.range(cluster1).forEach(function(el, i) {
var obj = {};
obj.cluster = 0;
obj.radius = 2;
obj.colour = '#9CCFE5';
obj.x = width/2 + (Math.random() - 10);
obj.y = height/2 + (Math.random());
nodes.push(obj);
}); // get men nodes
d3.range(cluster2).forEach(function(el, i) {
var obj = {};
obj.cluster = 1;
obj.radius = 2;
obj.colour = '#9FE789';
obj.x = width/2 + (Math.random() + 10);
obj.y = height/2 + (Math.random());
nodes.push(obj);
}); // get women nodes
initSimulation(nodes); // kick off simulation
} // getSimulationData()
// === Set up simulation params === //
function initSimulation(nodes) {
simulation = d3.forceSimulation(nodes)
.alpha(0.3)
.force('charge', d3.forceManyBody().strength(-5))
.force('xPos', d3.forceX(function(d) { return d.cluster === 0 ? width * 0.45 : width * 0.45; }).strength(0.7) )
.force('yPos', d3.forceY(height/2).strength(1))
.force('collide', d3.forceCollide(2));
simulation.on('tick', ticked);
function ticked() {
context.clearRect(0, 0, width, height);
context.save();
nodes.forEach(drawNode);
context.restore()
} // ticked()
function drawNode(d) {
if (bounded) {
// keep the nodes with the canvas bounds. Remove to let them free...
d.x = Math.max(d.radius, Math.min(width - d.radius, d.x));
d.y = Math.max(d.radius, Math.min(height - d.radius, d.y));
}
context.beginPath();
context.moveTo(d.x + d.radius, d.y);
context.arc(d.x, d.y, d.radius, 0, 2 * Math.PI);
context.fillStyle = d.colour;
context.fill();
} // drawNode()
} // Set up the simulation
// === Listeners / Handlers === //
d3.select('#submit').on('mousedown', function() {
var cluster1 = parseInt(document.querySelector('#cluster1').value, 10);
var cluster2 = parseInt(document.querySelector('#cluster2').value, 10);
getSimulationData(cluster1, cluster2);
}); // submit listener/handler - update new data
d3.select('button#split').on('mousedown', function(d) {
simulation.stop();
simulation
.force('xPos', d3.forceX(function(d) { return d.cluster === 0 ? width * 0.3 : width * 0.7; }).strength(0.7) )
.force('yPos', d3.forceY(height/2).strength(0.7));
simulation.alpha(0.2);
simulation.restart();
}); // split button listener/handler
d3.select('button#unite').on('mousedown', function(d) {
simulation.stop();
simulation
.force('xPos', d3.forceX(function(d) { return d.cluster === 0 ? width * 0.5 : width * 0.5; }).strength(0.5))
.force('yPos', d3.forceY(height/2).strength(0.5));
simulation.alpha(0.2);
simulation.restart();
}); // unite button listener/handler
d3.select('button#bounded').on('mousedown', function(d) {
simulation.stop();
bounded = bounded === false ? true : false;
var b = d3.select('#bounded');
bounded ? b.html('Box-bound') : b.html('Not Box-bound');
simulation.restart();
}); // button listener/handler
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment