Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active November 25, 2019 20:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save veltman/38149d05ea247cbcebb1 to your computer and use it in GitHub Desktop.
Save veltman/38149d05ea247cbcebb1 to your computer and use it in GitHub Desktop.
Hexbin resizing test

Stress testing resizing hexagon bins on the fly with d3-hexbin and canvas. 41k random 311 service request locations.

Currently relies on Path2D() so that probably excludes a bunch of browsers.

See also: Dynamic Hexbin

(function() {
d3.hexbin = function() {
var width = 1,
height = 1,
r,
x = d3_hexbinX,
y = d3_hexbinY,
dx,
dy;
function hexbin(points) {
var binsById = {};
points.forEach(function(point, i) {
var py = y.call(hexbin, point, i) / dy, pj = Math.round(py),
px = x.call(hexbin, point, i) / dx - (pj & 1 ? .5 : 0), pi = Math.round(px),
py1 = py - pj;
if (Math.abs(py1) * 3 > 1) {
var px1 = px - pi,
pi2 = pi + (px < pi ? -1 : 1) / 2,
pj2 = pj + (py < pj ? -1 : 1),
px2 = px - pi2,
py2 = py - pj2;
if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2;
}
var id = pi + "-" + pj, bin = binsById[id];
if (bin) bin.push(point); else {
bin = binsById[id] = [point];
bin.i = pi;
bin.j = pj;
bin.x = (pi + (pj & 1 ? 1 / 2 : 0)) * dx;
bin.y = pj * dy;
}
});
return d3.values(binsById);
}
function hexagon(radius) {
var x0 = 0, y0 = 0;
return d3_hexbinAngles.map(function(angle) {
var x1 = Math.sin(angle) * radius,
y1 = -Math.cos(angle) * radius,
dx = x1 - x0,
dy = y1 - y0;
x0 = x1, y0 = y1;
return [dx, dy];
});
}
hexbin.x = function(_) {
if (!arguments.length) return x;
x = _;
return hexbin;
};
hexbin.y = function(_) {
if (!arguments.length) return y;
y = _;
return hexbin;
};
hexbin.hexagon = function(radius) {
if (arguments.length < 1) radius = r;
return "m" + hexagon(radius).join("l") + "z";
};
hexbin.centers = function() {
var centers = [];
for (var y = 0, odd = false, j = 0; y < height + r; y += dy, odd = !odd, ++j) {
for (var x = odd ? dx / 2 : 0, i = 0; x < width + dx / 2; x += dx, ++i) {
var center = [x, y];
center.i = i;
center.j = j;
centers.push(center);
}
}
return centers;
};
hexbin.mesh = function() {
var fragment = hexagon(r).slice(0, 4).join("l");
return hexbin.centers().map(function(p) { return "M" + p + "m" + fragment; }).join("");
};
hexbin.size = function(_) {
if (!arguments.length) return [width, height];
width = +_[0], height = +_[1];
return hexbin;
};
hexbin.radius = function(_) {
if (!arguments.length) return r;
r = +_;
dx = r * 2 * Math.sin(Math.PI / 3);
dy = r * 1.5;
return hexbin;
};
return hexbin.radius(1);
};
var d3_hexbinAngles = d3.range(0, 2 * Math.PI, Math.PI / 3),
d3_hexbinX = function(d) { return d[0]; },
d3_hexbinY = function(d) { return d[1]; };
})();
<!DOCTYPE html>
<meta charset="utf-8">
<style>
canvas {
position: absolute;
left: 0;
top: 0;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js"></script>
<script src="hexbin.js"></script>
<script>
var canvas = d3.select("body").append("canvas")
context = d3.select("canvas").node().getContext("2d");
// NY state plane
var projection = d3.geo.conicConformal()
.parallels([40 + 2 / 3, 41 + 1 / 30])
.rotate([74, 40 + 1 / 6]);
var path = d3.geo.path()
.projection(projection);
// ColorBrewer purples
var color = d3.scale.quantile()
.range(["#feebe2","#fbb4b9","#f768a1","#c51b8a","#7a0177"]);
// How long before switching direction, in ms
var interval = 2000;
// Scale for hexagon radius
var radius = d3.scale.linear().domain([0,interval]).range([2,30]);
var clip;
var hexbinner = d3.hexbin();
// Get data
queue()
.defer(d3.json,"nyc.geojson")
.defer(d3.csv,"service-requests-311.csv") // 41k random 311 service request locations
.await(ready);
function ready(err,nyc,points) {
points.forEach(function(p){
p.lng = +p.lng;
p.lat = +p.lat;
});
// Keep canvas responsive
window.onresize = resize;
resize();
// Redraw canvas
function update() {
var bins = hexbinner(points.map(function(p){
return projection([p.lng,p.lat]);
}));
color.domain(bins.map(function(b){
return b.length;
}));
context.fillStyle = "#fff";
context.fill(clip);
context.globalCompositeOperation = "source-atop";
var hex = new Path2D(hexbinner.hexagon());
bins.forEach(function(bin){
context.translate(bin.x,bin.y);
context.fillStyle = color(bin.length);
context.fill(hex);
context.setTransform(1, 0, 0, 1, 0, 0);
});
context.stroke(clip);
}
// Update the hex radius based on time and redraw
// Reverse scale every other interval
function animate(t) {
var progress = t % interval,
cycle = Math.floor(t/interval);
if (cycle % 2) {
progress = interval - progress;
}
hexbinner.radius(radius(progress));
update();
window.requestAnimationFrame(animate);
}
window.requestAnimationFrame(animate);
// Get new window size, update all dimensions + projection
function resize() {
var width = window.innerWidth,
height = window.innerHeight;
hexbinner.size([width, height]);
canvas.attr("width",width)
.attr("height",height);
context.clearRect(0,0,width,height);
projection.scale(1)
.translate([0,0]);
var b = path.bounds(nyc),
s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
projection
.scale(s)
.translate(t);
clip = new Path2D(path(nyc));
}
}
</script>
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.
View raw

(Sorry about that, but we can’t show files that are this big right now.)

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