Skip to content

Instantly share code, notes, and snippets.

Created October 10, 2016 20:05
Show Gist options
  • Save anonymous/a41991f7c81edd6fc74f55e69ca4d302 to your computer and use it in GitHub Desktop.
Save anonymous/a41991f7c81edd6fc74f55e69ca4d302 to your computer and use it in GitHub Desktop.
Weighted Voronoi (sort of)
license: mit

A playful variation on this example about collision detection, aiming to create a sort of weighted Voronoi tessellation. The center of each circle is used as an input vertex for d3.geom.voronoi (see the docs).

Some cells are indeed bigger than other ones, but the resulting area is in general not proportional to the intended value.

The non-overlapping circles used to generate the tessellation can be seen as translucent bubbles. You can also adjust the padding between them by using the slider.

forked from nitaku's block: Weighted Voronoi (sort of)

.cell {
fill: #fff;
stroke: #00A699;
stroke-width: 2;
}
.node {
fill: #00A699;
opacity: 0.05;
}
.axis .domain {
fill: none;
stroke: #000;
stroke-opacity: .3;
stroke-width: 10px;
stroke-linecap: round;
}
.axis .halo {
fill: none;
stroke: #ddd;
stroke-width: 8px;
stroke-linecap: round;
}
.slider .handle {
fill: #fff;
stroke: #000;
stroke-opacity: .5;
stroke-width: 1.25px;
cursor: crosshair;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v3.min.js"></script>
<link rel="stylesheet" type="text/css" href="index.css">
<title>Weighted Voronoi (sort of)</title>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
var width = 960,
height = 500,
padding = 2,
min_padding = 0,
max_padding = 50,
maxRadius = 17,
n = 200;
var points = [
[18.5,15.7826087],
[18.5,15.1630435 ],
[18.5,14.75 ],
[17.8928571,13.3043478 ],
[17.4880952,12.2717391 ],
[13.8452381,4.63043478 ],
[12.6309524,2.35869565 ],
[11.8214286,0.913043478 ],
[11.0119048,0.5 ],
[10,0.5 ],
[8.98809524,0.5 ],
[8.17857143,0.913043478 ],
[7.36904762,2.35869565 ],
[6.15476191,4.63043478],
[2.51190476,12.2717391 ],
[2.10714286,13.3043478 ],
[1.5,14.75 ],
[1.5,15.1630435],
[1.5,15.7826087 ],
[1.5,18.4673913 ],
[3.52380952,19.5 ],
[5.14285714,19.5 ],
[8.78571429,19.5 ],
[12.8333333,13.5108696],
[12.8333333,10.5 ],
[12.8333333,9.17391304],
[12.0238097,7.52173913 ],
[10,7.5 ],
[7.97619034,7.52173913 ],
[7.16666667,9.17391304 ],
[7.16666667,10.5 ],
[7.16666667,13.5108696],
[11.2142857,19.5 ],
[14.8571429,34.5 ],
[16.4761905,19.5 ],
[18.5,18.4673913 ],
[18.5,15.7826087]
];
var nodes = points.map(function(d, i) {
var r = Math.sqrt(1 / 1 * -Math.log(Math.random())) * maxRadius,
p = {id: i, radius: r, cx: (d[0]*10) + 350, cy: (d[1]*10) + 150};
return p;
});
nodes.forEach(function(d) { d.x = d.cx; d.y = d.cy; });
var voronoi = d3.geom.voronoi()
.clipExtent([[-padding,-padding],[width+padding,height+padding]])
.x(function(d){ return d.x; })
.y(function(d){ return d.y; });
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var cells = svg.selectAll('.cell')
.data(voronoi(nodes));
cells.enter().append('path')
.attr('class', 'cell');
var circle = svg.selectAll("circle")
.data(nodes);
var enter_circle = circle.enter().append("circle")
.attr('class', 'node');
enter_circle
.attr("r", function(d) { return d.radius; })
.attr("cx", function(d) { return d.cx; })
.attr("cy", function(d) { return d.cy; });
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
force.alpha(.05);
function tick(e) {
circle
.each(gravity(.2 * e.alpha))
.each(collide(.5))
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
cells
.data(voronoi(nodes));
cells
.attr('d', function(d){
if (d.length > 0)
return "M" + d.join("L") + "Z";
else {
return '';
}
});
}
/* SLIDER
*/
var x = d3.scale.linear()
.domain([min_padding, max_padding])
.range([0, width/2])
.clamp(true);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(10, 10)")
.call(d3.svg.axis()
.scale(x)
.ticks(0)
.tickSize(0))
.select(".domain")
.select(function() { return this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "halo");
var brush = d3.svg.brush()
.x(x)
.extent([0, 0])
.on("brush", brushed);
var slider = svg.append("g")
.attr("class", "slider")
.call(brush);
slider.selectAll(".extent,.resize")
.remove();
var handle = slider.append("circle")
.attr("class", "handle")
.attr("transform", "translate(10, 10)")
.attr("r", 9);
brush.extent([padding, padding]);
slider
.call(brush.event);
function brushed() {
var value = brush.extent()[0];
if (d3.event.sourceEvent) {
value = x.invert(d3.mouse(this)[0]);
brush.extent([value, value]);
force.alpha(.01);
}
handle.attr("cx", x(value));
padding = value;
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment