Skip to content

Instantly share code, notes, and snippets.

@awoodruff
Last active September 26, 2021 21:50
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save awoodruff/94dc6fc7038eba690f43 to your computer and use it in GitHub Desktop.
Save awoodruff/94dc6fc7038eba690f43 to your computer and use it in GitHub Desktop.
Dot density map

This is an attempt at a slightly D3-ish way of drawing a dot density map on a canvas element. In short, each polygon is drawn (invisibly) with a unique color, then random points are thrown at polygon bounding boxes and the pixel color is used for a point-in-polygon test. This example draws one dot for every 2 people in central Boston census blocks, for a total of approximately 132,000 dots.

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.
<html>
<head>
<title>Dot density map</title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
</head>
<body>
<script>
var width = 1160,
height = 700;
// invisible map of polygons
var polyCanvas = d3.select("body")
.append("canvas")
.attr("width",width)
.attr("height",height)
.style("display","none");
// using this div to crop the map; it has messy edges
var container = d3.select("body")
.append("div")
.style({
"position": "relative",
"width": (width-200) + "px",
"height": (height-200) + "px",
"overflow": "hidden"
});
// canvas for dot map
var dotCanvas = container
.append("canvas")
.attr("width",width)
.attr("height",height)
.style({
"position": "absolute",
"top": "-100px",
"left": "-100px"
});
var projection = d3.geo.albers()
.rotate([71.083,0])
.center([0,42.3581])
.parallels([40,44])
.scale(880000)
.translate([width / 2, height / 2]);
var path = d3.geo.path().projection(projection);
var polyContext = polyCanvas.node().getContext("2d"),
dotContext = dotCanvas.node().getContext("2d");
var features;
d3.json( "blocks.json", function(error, blocks){
features = topojson.feature(blocks, blocks.objects.massblocks_central).features;
// draw the polygons with a unique color for each
var i=features.length;
while(i--){
var r = parseInt(i / 256),
g = i % 256;
drawPolygon( features[i], polyContext, "rgb(" + r + "," + g + ",0)" );
};
// pixel data for the whole polygon map. we'll use color for point-in-polygon tests.
var imageData = polyContext.getImageData(0,0,width,height);
// now draw dots
i=features.length;
while(i--){
var pop = features[i].properties.POP10 / 2; // one dot = 2 people
if ( !pop ) continue;
var bounds = path.bounds(features[i]),
x0 = bounds[0][0],
y0 = bounds[0][1],
w = bounds[1][0] - x0,
h = bounds[1][1] - y0,
hits = 0,
count = 0,
limit = pop*10, // limit tests just in case of infinite loops
x,
y,
r = parseInt(i / 256),
g = i % 256;
// test random points within feature bounding box
while( hits < pop-1 && count < limit ){ // we're done when we either have enough dots or have tried too many times
x = parseInt(x0 + Math.random()*w);
y = parseInt(y0 + Math.random()*h);
// use pixel color to determine if point is within polygon. draw the dot if so.
if ( testPixelColor(imageData,x,y,width,r,g) ){
drawPixel(x,y,0,153,204,255); // #09c, vintage @indiemaps
hits++;
}
count ++;
}
}
});
function testPixelColor(imageData,x,y,w,r,g){
var index = (x + y * w) * 4;
return imageData.data[index + 0] == r && imageData.data[index + 1] == g;
}
function drawPolygon( feature, context, fill ){
var coordinates = feature.geometry.coordinates;
context.fillStyle = fill || "#000";
context.beginPath();
coordinates.forEach( function(ring){
ring.forEach( function(coord, i){
var projected = projection( coord );
if (i == 0) {
context.moveTo(projected[0], projected[1]);
} else {
context.lineTo(projected[0], projected[1]);
}
});
});
context.closePath();
context.fill();
}
// there are faster (or prettier) ways to draw lots of dots, but this works
function drawPixel (x, y, r, g, b, a) {
dotContext.fillStyle = "rgba("+r+","+g+","+b+","+(a/255)+")";
dotContext.fillRect( x, y, 1, 1 );
}
</script>
</body>
</html>
@hughes036
Copy link

Thanks a lot for making this available, I am getting some useful ideas from it! Could you tell me where the data (shapes and pop) came from, and what you had to do to get it in this format?

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment