Skip to content

Instantly share code, notes, and snippets.

@williaster
Last active October 10, 2016 23:26
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 williaster/926e39ce3a80dfc26af0a443d4961c81 to your computer and use it in GitHub Desktop.
Save williaster/926e39ce3a80dfc26af0a443d4961c81 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)

forked from anonymous'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