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
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> |
(Sorry about that, but we can’t show files that are this big right now.)