Skip to content

Instantly share code, notes, and snippets.

@joshcarr
Forked from mbostock/.block
Last active August 24, 2023 04:09
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save joshcarr/c0a405243d3051a9c986 to your computer and use it in GitHub Desktop.
Save joshcarr/c0a405243d3051a9c986 to your computer and use it in GitHub Desktop.
Google Maps + D3 + hexbin

This example shows how to use the d3.hexbin plugin for hexagonal binning on GoogleMaps. It is an update of Mike Bostock's Google Maps + D3 block.

(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>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no"/>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script type="text/javascript" src="hexbin.js"></script>
<style type="text/css">
html, body, #map {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.stations, .stations svg {
position: absolute;
}
.stations svg {
width: 60px;
height: 20px;
padding-right: 100px;
font: 10px sans-serif;
}
.stations circle {
fill: brown;
stroke: black;
stroke-width: 1.5px;
}
</style>
</head>
<body>
<div id="map"></div>
<script type="text/javascript">
// Create the Google Map…
var map = new google.maps.Map(d3.select("#map").node(), {
zoom: 8,
center: new google.maps.LatLng(37.76487, -122.41948),
mapTypeId: google.maps.MapTypeId.TERRAIN
});
// Load the station data. When the data comes back, create an overlay.
d3.json("stations.json", function(data) {
var overlay = new google.maps.OverlayView();
// Add the container when the overlay is added to the map.
overlay.onAdd = function() {
var layer = d3.select(this.getPanes().overlayLayer).append("div")
.attr("class", "stations");
var svg = d3.select(this.getPanes().overlayLayer)
.append('svg')
.attr("class", "hex");
var levels = {};
var curLevel = false;
// Draw each marker as a separate SVG element.
// We could use a single SVG, but what size would it have?
overlay.draw = function() {
var projection = this.getProjection(),
padding = 10;
var marker = layer.selectAll("svg")
.data(d3.entries(data))
.each(transform) // update existing markers
.enter().append("svg:svg")
.each(transform)
.attr("class", "marker");
// Add a circle.
marker.append("svg:circle")
.attr("r", 4.5)
.attr("cx", padding)
.attr("cy", padding);
// Add a label.
marker.append("svg:text")
.attr("x", padding + 7)
.attr("y", padding)
.attr("dy", ".31em")
.text(function(d) { return d.key; });
function transform(d) {
d = new google.maps.LatLng(d.value[1], d.value[0]);
d = projection.fromLatLngToDivPixel(d);
return d3.select(this)
.style("left", (d.x - padding) + "px")
.style("top", (d.y - padding) + "px");
}
var hexRadius = 40;
var hexPad = 100;
var layout = d3.hexbin().radius(hexRadius);
var rscale = d3.scale.sqrt().range([0, hexRadius]).clamp(true);
var cscale = d3.scale.linear().domain([0,20]).range(["#00FF00","#FFA500"]);
function hexbinStyle(hexagons) {
hexagons
.attr("stroke", "rgba(0,0,0,0.5)")
.attr("stroke-width", "0.2px")
.attr("opacity", "0.75")
.attr("fill", function (d) {
var avg = d3.median(d, function(d) {
return +d[2].value[3][0];
// return +d[2].o3;
});
return cscale(avg);
});
}
function genHexagons (container) {
var hexData = d3.entries(data).map(function (d) {
var latlng = new google.maps.LatLng(d.value[1], d.value[0]);
var px = projection.fromLatLngToDivPixel(latlng);
return [px.x, px.y, d];
}, this);
var bins = layout(hexData);
var hexagons = container.selectAll(".hexagon").data(bins);
var counts = [];
bins.map(function (elem) { counts.push(elem.length); });
rscale.domain([0, (d3.mean(counts) + (d3.deviation(counts) * 3))]);
var path = hexagons.enter().append("path").attr("class", "hexagon");
hexbinStyle.call(this, path);
hexagons
.attr("d", function(d) {
return layout.hexagon(hexRadius);
})
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
var zoom = map.getZoom();
var bounds = new google.maps.LatLngBounds();
d3.entries(data).map(function (d) {
var latlng = new google.maps.LatLng(d.value[1], d.value[0]);
bounds.extend(latlng);
});
var topRight = projection.fromLatLngToDivPixel( bounds.getNorthEast() );
var bottomLeft = projection.fromLatLngToDivPixel( bounds.getSouthWest() );
var sizeX = topRight.x - bottomLeft.x;
var sizeY = bottomLeft.y - topRight.y;
svg.attr('width', sizeX + (2 * hexPad))
.attr('height', sizeY + (2 * hexPad))
.style("margin-left", (bottomLeft.x - hexPad) + "px")
.style("margin-top", (topRight.y - hexPad) + "px");
if (!(zoom in levels)) {
levels[zoom] = svg.append("g").attr("class", "zoom-" + zoom);
genHexagons(levels[zoom]);
levels[zoom].attr("transform", "translate(" + -(bottomLeft.x - hexPad) + "," + -(topRight.y - hexPad) + ")");
}
if (curLevel) {
curLevel.style("display", "none");
}
curLevel = levels[zoom];
curLevel.style("display", "inline");
};
};
// Bind our overlay to the map…
overlay.setMap(map);
});
</script>
</body>
</html>
{"KMAE":[-120.12,36.98,"MADERA MUNICIPAL AIRPORT",[26,1,2,5,6,3,2,1,2,7,29,12,3]],"KSJC":[-121.92,37.37,"SAN JOSE INTERNATIONAL AIRPORT",[28,1,1,1,6,10,5,3,2,4,14,21,7]],"KMCE":[-120.50,37.28,"MERCED MUNICIPAL AIRPORT",[29,1,1,3,7,5,2,1,3,6,12,26,5]],"KMER":[-120.57,37.37,"Merced / Castle Air Force Base",[34,1,1,1,4,5,2,1,1,4,17,22,7]],"KAPC":[-122.28,38.20,"NAPA COUNTY AIRPORT",[23,2,1,6,3,3,8,18,11,13,4,3,5]],"KSUU":[-121.95,38.27,"Fairfield / Travis Air Force Base",[13,7,4,3,3,6,4,13,33,4,1,2,7]],"KSQL":[-122.25,37.52,"San Carlos Airport",[18,3,2,2,3,4,3,2,5,17,16,12,12]],"KSNS":[-121.60,36.67,"SALINAS MUNICIPAL AIRPORT",[21,1,1,6,12,3,1,2,9,21,17,5,1]],"KMOD":[-120.95,37.62,"MODESTO CITY CO SHAM FLD",[27,1,1,2,10,5,1,1,1,3,17,24,8]],"KOAK":[-122.23,37.72,"METRO OAKLAND INTERNATIONAL AIRPORT ",[16,3,3,2,4,6,3,4,9,23,20,6,2]],"KSCK":[-121.23,37.90,"STOCKTON METROPOLITAN AIRPORT ",[21,2,2,3,6,8,2,1,4,15,19,12,4]],"KCCR":[-122.05,38.00,"CONCORD BUCHANAN FIELD",[24,3,2,1,1,5,17,12,9,9,7,6,4]],"KMRY":[-121.85,36.58,"MONTEREY PENINSULA AIRPORT",[26,1,2,9,5,3,4,9,13,14,9,4,1]],"KPAO":[-122.12,37.47,"Palo Alto Airport",[31,3,1,1,2,5,1,1,1,4,10,25,14]],"KSAC":[-121.50,38.50,"SACRAMENTO EXECUTIVE AIRPORT ",[32,1,0,1,3,11,12,16,5,2,4,9,3]],"KHWD":[-122.12,37.67,"HAYWARD AIR TERMINAL",[20,2,7,2,2,6,3,3,6,23,18,6,2]],"KSTS":[-122.82,38.50,"SANTA ROSA SONOMA COUNTY",[46,1,0,1,5,13,10,4,3,3,4,6,3]],"KSMF":[-121.60,38.70,"SACRAMENTO INTERNATIONAL AIRPORT",[19,2,1,2,4,21,18,8,3,2,5,12,4]],"KNUQ":[-122.05,37.43,"MOFFETT FIELD",[35,3,1,1,4,7,2,1,2,5,6,17,15]],"KRHV":[-121.82,37.33,"San Jose / Reid / Hillv",[35,0,0,1,4,4,2,1,1,10,28,11,1]],"KWVI":[-121.78,36.93,"WATSONVILLE MUNICIPAL AIRPORT ",[44,1,2,3,4,5,7,9,8,4,6,5,2]],"KMHR":[-121.30,38.55,"Sacramento, Sacramento Mather Airport",[21,1,1,2,8,15,12,12,7,4,5,7,3]],"KVCB":[-121.95,38.38,"VACAVILLE NUT TREE AIRPORT",[36,2,1,1,2,6,10,18,10,2,2,5,6]],"KSFO":[-122.37,37.62,"SAN FRANCISCO INTERNATIONAL AIRPORT ",[13,3,3,2,3,4,4,4,7,31,20,2,3]],"KLVK":[-121.82,37.70,"LIVERMORE MUNICIPAL AIRPORT ",[32,2,7,3,1,1,2,7,9,17,16,2,1]]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment