Skip to content

Instantly share code, notes, and snippets.

@shashashasha
Created June 11, 2011 00:50
Show Gist options
  • Save shashashasha/1020117 to your computer and use it in GitHub Desktop.
Save shashashasha/1020117 to your computer and use it in GitHub Desktop.
Circle Packing

An implementation of Circle Packing based on the MooCirclePack from back in the day. There is no energy or accounting for re-dragging points in this version, it just simply loops several times over. It's a bit simple but it seems to do the thing.

We've used this script in a couple places, for both our Android vs iPhone visualization and our recent Crime Maps launch.

// based off of http://labs.unwieldy.net/moocirclepack/
org.polymaps.packer = function() {
var packer = {},
nodes = [],
elements = [],
timesToPack = 50;
packer.elements = function(e) {
if (!arguments.length) {
return elements;
}
elements = e;
return packer;
};
packer.start = function() {
populateNodes();
var j = 0;
var intervalId = setInterval(function() {
// limit the number of times it packs
if (j++ > timesToPack) {
clearInterval(intervalId);
intervalId = null;
}
draw();
}, 40);
return packer;
};
var populateNodes = function() {
nodes = [];
for (var i = 0; i < elements.length; i++)
{
// thanks to zain + mbostock for making me less afraid of regexes
var at = elements[i].getAttribute("transform"),
av = at.match(/translate\((.*),(.*)\)/);
ax = Number(av[1]),
ay = Number(av[2]),
ar = elements[i].getAttribute("r");
var n = {
x: ax,
y: ay,
r: parseFloat(ar),
id: i
};
nodes[i] = n;
}
};
var draw = function() {
for (var i = 0; i < elements.length; i++) {
for (var j = 0; j < elements.length; j++) {
if (i == j) {
continue;
}
pack(nodes[i], nodes[j]);
}
var e = elements[i];
var n = nodes[i];
e.setAttribute("transform", "translate(" + n.x + "," + n.y + ")");
}
};
// circle pack
// if the radii intersect, push them apart
var pack = function(a, b) {
if (intersects(a, b)) {
v.x = a.x - b.x;
v.y = a.y - b.y;
var d = (v.x * v.x) + (v.y * v.y);
v.normalize();
v.mult((a.r + b.r + 1 - Math.sqrt(d)) * 0.25);
a.x += v.x;
a.y += v.y;
b.x -= v.x;
b.x -= v.y;
}
};
// vector
var v = {};
v.normalize = function() {
v.magnitude = Math.sqrt((v.x * v.x) + (v.y * v.y));
v.x = v.x / v.magnitude;
v.y = v.y / v.magnitude;
};
v.mult = function(m) {
v.x *= m;
v.y *= m;
};
var distance = function(ax, ay, bx, by) {
var dx = ax - bx;
var dy = ay - by;
return Math.sqrt((dx*dx) + (dy*dy));
};
var intersects = function(a, b) {
var d = distance(a.x, a.y, b.x, b.y);
return (d < (a.r + b.r + 1));
};
return packer;
};
<html>
<head>
<title>Circle Packing</title>
<script type="text/javascript" src="https://raw.github.com/simplegeo/polymaps/master/polymaps.js"></script>
<script type="text/javascript" src="circlepacker.js"></script>
<style type="text/css">
body {
margin: 0;
}
svg {
width: 100%;
height: 100%;
}
.compass .back {
fill: #eee;
fill-opacity: .8;
}
.compass .fore {
stroke: #999;
stroke-width: 1.5px;
}
.compass rect.back.fore {
fill: #999;
fill-opacity: .3;
stroke: #eee;
stroke-width: 1px;
}
.compass .direction {
fill: none;
}
.compass .chevron {
fill: none;
stroke: #999;
stroke-width: 5px;
}
.compass .zoom .chevron {
stroke-width: 4px;
}
.layer circle:hover {
fill: #999;
}
</style>
</head>
<body>
<div id="map">
</div>
<script type="text/javascript">
var points = [];
for (var i = 0; i < 100; i++) {
var rlat = Math.random() * 14 + 32;
var rlon = Math.random() * 40 - 120;
var rrad = Math.random() * 20 + 3;
var feature = {
type: "Feature",
geometry: {
type: "Point",
coordinates: [rlon, rlat]
},
properties: {
size: rrad
}
};
points.push(feature);
}
var po = org.polymaps,
packer = po.packer(),
div = document.getElementById("map");
var map = po.map()
.container(div.appendChild(po.svg("svg")))
.center({lat: 38, lon: -95})
.zoom(4);
map.add(po.image()
.url(po.url("http://{S}tile.cloudmade.com"
+ "/5814d279db61404b9e52115b06b9e7b3" // http://cloudmade.com/register
+ "/22677/256/{Z}/{X}/{Y}.png")
.hosts(["a.", "b.", "c.", ""])));
map.add(po.geoJson()
.on("load", loadPoints)
.features(points));
map.add(po.interact());
map.add(po.compass().pan("none"));
function loadPoints(e) {
var tile = e.tile,
g = tile.element,
elements = [];
for (var i = 0; i < e.features.length; i++) {
var element = e.features[i].element;
element.setAttribute("r", e.features[i].data.properties.size);
elements.push(element);
}
if (elements.length) {
packer.elements(elements).start();
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment