Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active August 29, 2023 06:47
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save mbostock/5249328 to your computer and use it in GitHub Desktop.
Save mbostock/5249328 to your computer and use it in GitHub Desktop.
Hexagon Mesh
license: gpl-3.0

Click and drag above to paint red hexagons. A black outline will appear around contiguous clusters of red hexagons. This outline is constructed using topojson.mesh, part of the TopoJSON client API. A filter is specified so that the mesh only contains boundaries that separate filled hexagons from empty hexagons.

The hexagon grid itself is represented as TopoJSON, but is constructed on-the-fly in the browser. Since TopoJSON requires quantized coordinates, the hexagon grid is represented as integers, with each hexagon of dimensions 3×2. Then a custom projection is used to transform these irregular integer hexagons to normal hexagons of the desired size.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.hexagon {
fill: white;
pointer-events: all;
}
.hexagon path {
-webkit-transition: fill 250ms linear;
transition: fill 250ms linear;
}
.hexagon :hover {
fill: pink;
}
.hexagon .fill {
fill: red;
}
.mesh {
fill: none;
stroke: #000;
stroke-opacity: .2;
pointer-events: none;
}
.border {
fill: none;
stroke: #000;
stroke-width: 2px;
pointer-events: none;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var width = 960,
height = 500,
radius = 20;
var topology = hexTopology(radius, width, height);
var projection = hexProjection(radius);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("class", "hexagon")
.selectAll("path")
.data(topology.objects.hexagons.geometries)
.enter().append("path")
.attr("d", function(d) { return path(topojson.feature(topology, d)); })
.attr("class", function(d) { return d.fill ? "fill" : null; })
.on("mousedown", mousedown)
.on("mousemove", mousemove)
.on("mouseup", mouseup);
svg.append("path")
.datum(topojson.mesh(topology, topology.objects.hexagons))
.attr("class", "mesh")
.attr("d", path);
var border = svg.append("path")
.attr("class", "border")
.call(redraw);
var mousing = 0;
function mousedown(d) {
mousing = d.fill ? -1 : +1;
mousemove.apply(this, arguments);
}
function mousemove(d) {
if (mousing) {
d3.select(this).classed("fill", d.fill = mousing > 0);
border.call(redraw);
}
}
function mouseup() {
mousemove.apply(this, arguments);
mousing = 0;
}
function redraw(border) {
border.attr("d", path(topojson.mesh(topology, topology.objects.hexagons, function(a, b) { return a.fill ^ b.fill; })));
}
function hexTopology(radius, width, height) {
var dx = radius * 2 * Math.sin(Math.PI / 3),
dy = radius * 1.5,
m = Math.ceil((height + radius) / dy) + 1,
n = Math.ceil(width / dx) + 1,
geometries = [],
arcs = [];
for (var j = -1; j <= m; ++j) {
for (var i = -1; i <= n; ++i) {
var y = j * 2, x = (i + (j & 1) / 2) * 2;
arcs.push([[x, y - 1], [1, 1]], [[x + 1, y], [0, 1]], [[x + 1, y + 1], [-1, 1]]);
}
}
for (var j = 0, q = 3; j < m; ++j, q += 6) {
for (var i = 0; i < n; ++i, q += 3) {
geometries.push({
type: "Polygon",
arcs: [[q, q + 1, q + 2, ~(q + (n + 2 - (j & 1)) * 3), ~(q - 2), ~(q - (n + 2 + (j & 1)) * 3 + 2)]],
fill: Math.random() > i / n * 2
});
}
}
return {
transform: {translate: [0, 0], scale: [1, 1]},
objects: {hexagons: {type: "GeometryCollection", geometries: geometries}},
arcs: arcs
};
}
function hexProjection(radius) {
var dx = radius * 2 * Math.sin(Math.PI / 3),
dy = radius * 1.5;
return {
stream: function(stream) {
return {
point: function(x, y) { stream.point(x * dx / 2, (y - (2 - (y & 1)) / 3) * dy / 2); },
lineStart: function() { stream.lineStart(); },
lineEnd: function() { stream.lineEnd(); },
polygonStart: function() { stream.polygonStart(); },
polygonEnd: function() { stream.polygonEnd(); }
};
}
};
}
</script>
@Maltretieren
Copy link

Thanks for this great work. I use the algorithm (line 104 to 124) to generate a hexagonal grid for a map. I found that after generating the grid the hexagons in the last row are not closed. Couldn't figure out how to fix this. Anybody an idea?

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