block by nitaku 616c4ef99f71d3dab203

Weighted Voronoi (sort of)

Full Screen

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.

index.js

var width = 960,
    height = 500,
    padding = 2,
    min_padding = 0,
    max_padding = 50,
    maxRadius = 30,
    n = 200;

var nodes = d3.range(n).map(function(i) {
  var r = Math.sqrt(1 / 1 * -Math.log(Math.random())) * maxRadius,
      	d = {id: i, radius: r, cx: width/2+Math.random()*600-300, cy: height/2+Math.random()*200-100};
  return d;
});

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;
	};
}

index.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<script src="//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>

index.css

.cell {
	fill: #C9DEEC;
	stroke: white;
	stroke-width: 2;
}
.node {
	fill: steelblue;
	opacity: 0.1;
}

.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;
}