..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:
d3.forceManyBody().strength(-5)
d3.forceCollide(2)
(2 is the radius)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
forked from larsvers's block: Force split/unite playground
forked from anonymous's block: Force split/unite playground
xxxxxxxxxx
<html lang="en">
<head>
<meta charset="utf-8">
<title>winter force</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://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>
Modified http://d3js.org/d3.v4.js to a secure url
https://d3js.org/d3.v4.js