Skip to content

Instantly share code, notes, and snippets.

@mtaptich
Last active January 27, 2016 23:50
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mtaptich/15be31d06f1c45991aaf to your computer and use it in GitHub Desktop.
Save mtaptich/15be31d06f1c45991aaf to your computer and use it in GitHub Desktop.
Dot Map + d3.js

A framework for creating Dot Maps in d3.js using d3.geom.quadtree and Mitchell’s Best-Candidate (MBC) algorithm as its backbone. Here, I show the percentage of California residents who rent on a county basis.

The rendering speed is very slow due to the recursive MBC calls and small circle areas. However, it is worth the wait if all you are trying to do is download a map for a report.

Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<style type="text/css">
.county-border {
fill: none;
stroke: #34495e;
stroke-opacity: .35;
}
.land {
fill: #ecf0f1;
}
</style>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="http://d3js.org/queue.v1.min.js"></script>
<script>
var margin = { top: 20, right: 20, bottom: 20, left: 20 },
width = 900 - margin.left - margin.right,
height = 970 - margin.top - margin.bottom,
k = 1, m = 500, n = 500;
var maxRadius = 1.5, // maximum circle size
padding = 1, // Padding between circles
newCircle = bestCircleGenerator(maxRadius, padding);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var path = d3.geo.path()
.projection(null);
queue()
.defer(d3.json, "california.json")
.await(ready);
function ready(error, ca){
svg.selectAll("path")
.data(topojson.feature(ca, ca.objects.county).features)
.enter().append("path")
.attr('class', 'land')
.attr("d", path)
.each(function(d,i){
// Generate a percentage
var goal = +d.properties.rent / (+d.properties.rent + +d.properties.own)
// Calculate the area of the geometry you wish to cover;
var a = path.area(d)*goal;
// Create the Dot Map
DotMap(d, a)
})
svg.append("path")
.datum(topojson.mesh(ca, ca.objects.county, function(a, b) { return a !== b; }))
.attr("class", "county-border")
.attr("d", path);
}
// https://gist.github.com/mbostock/1893974
function bestCircleGenerator(maxRadius, padding) {
var quadtree = d3.geom.quadtree().extent([[0, 0], [width, height]])([]),
searchRadius = maxRadius * 2;
return function(k, geometry) {
var bestX, bestY, bestDistance = 0,
pos = path.bounds(geometry),
w = pos[1][0] - pos[0][0],
h = pos[1][1] - pos[0][1];
for (var i = 0; i < k || bestDistance < padding; ++i) {
var x = Math.random() * w + pos[0][0],
y = Math.random() * h + pos[0][1],
rx1 = x - searchRadius,
rx2 = x + searchRadius,
ry1 = y - searchRadius,
ry2 = y + searchRadius,
minDistance = maxRadius; // minimum distance for this candidate
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (p = quad.point) {
var p,
dx = x - p[0],
dy = y - p[1],
d2 = dx * dx + dy * dy,
r2 = p[2] * p[2];
if (d2 < r2) return minDistance = 0, true; // within a circle
var d = Math.sqrt(d2) - p[2];
if (d < minDistance) minDistance = d;
}
return !minDistance || x1 > rx2 || x2 < rx1 || y1 > ry2 || y2 < ry1 ; // or outside search radius
});
if (minDistance > bestDistance) bestX = x, bestY = y, bestDistance = minDistance;
}
var best = [bestX, bestY, bestDistance - padding];
// Return the circle if it intersects our geometry
if (pointInGeometry([bestX, bestY], geometry)){
quadtree.add(best);
return best;
} else{
return []
}
};
}
// http://bl.ocks.org/mbostock/4218871
function pointInGeometry(point, poly) {
if (poly.geometry.type == 'MultiPolygon'){
var cor = poly.geometry.coordinates;
for (var sub = 0; sub < cor.length; sub++) {
var subcor = cor[sub][0]
for (var n = subcor.length, i = 0, j = n - 1, x = point[0], y = point[1], inside = false; i < n; j = i++) {
var xi = subcor[i][0], yi = subcor[i][1],
xj = subcor[j][0], yj = subcor[j][1];
if ((yi > y ^ yj > y) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) inside = !inside;
}
};
return inside;
} else {
var cor = poly.geometry.coordinates[0];
for (var n = cor.length, i = 0, j = n - 1, x = point[0], y = point[1], inside = false; i < n; j = i++) {
var xi = cor[i][0], yi = cor[i][1],
xj = cor[j][0], yj = cor[j][1];
if ((yi > y ^ yj > y) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) inside = !inside;
}
return inside;
}
}
// Goal is a percentage of the total geometry area
function DotMap(geometry, goal){
var area = 0, count = 0;
while (area < goal && count < 500){
var circle = newCircle(k, geometry);
if (circle[0]){
svg.append("circle")
.attr("cx", circle[0])
.attr("cy", circle[1])
.attr("r", 0)
.style("fill", '#e74c3c')
.attr("r", circle[2]);
area += circle[2]*circle[2]*Math.PI
}
if (k < 500) k *= 1.01, m *= .998;
count++
}
}
d3.select(self.frameElement).style("height", (height) + "px");
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment