|
<!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> |