Skip to content

Instantly share code, notes, and snippets.

@mlem
Forked from mbostock/.block
Last active October 21, 2015 23:38
Show Gist options
  • Select an option

  • Save mlem/b31f21f518112778cb5e to your computer and use it in GitHub Desktop.

Select an option

Save mlem/b31f21f518112778cb5e to your computer and use it in GitHub Desktop.
Hexagon Mesh

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.

var expectedResult = {
"transform": {
"translate": [
0,
0
],
"scale": [
1,
1
]
},
"objects": {
"hexagons": {
"type": "GeometryCollection",
"geometries": [
{
"type": "Polygon",
"arcs": [
[
3,
4,
5,
-19,
-2,
9
]
],
"fill": true
},
{
"type": "Polygon",
"arcs": [
[
6,
7,
8,
-22,
-5,
6
]
],
"fill": true
},
{
"type": "Polygon",
"arcs": [
[
9,
10,
11,
-25,
-8,
3
]
],
"fill": false
},
{
"type": "Polygon",
"arcs": [
[
18,
19,
20,
-31,
-17,
-3
]
],
"fill": true
},
{
"type": "Polygon",
"arcs": [
[
21,
22,
23,
-34,
-20,
-6
]
],
"fill": false
},
{
"type": "Polygon",
"arcs": [
[
24,
25,
26,
-37,
-23,
-9
]
],
"fill": false
},
{
"type": "Polygon",
"arcs": [
[
33,
34,
35,
-49,
-32,
-21
]
],
"fill": true
},
{
"type": "Polygon",
"arcs": [
[
36,
37,
38,
-52,
-35,
-24
]
],
"fill": false
},
{
"type": "Polygon",
"arcs": [
[
39,
40,
41,
-55,
-38,
-27
]
],
"fill": false
}
]
}
},
"arcs": [
[
[
-1,
-3
],
[
1,
1
]
],
[
[
0,
-2
],
[
0,
1
]
],
[
[
0,
-1
],
[
-1,
1
]
],
[
[
1,
-3
],
[
1,
1
]
],
[
[
2,
-2
],
[
0,
1
]
],
[
[
2,
-1
],
[
-1,
1
]
],
[
[
3,
-3
],
[
1,
1
]
],
[
[
4,
-2
],
[
0,
1
]
],
[
[
4,
-1
],
[
-1,
1
]
],
[
[
5,
-3
],
[
1,
1
]
],
[
[
6,
-2
],
[
0,
1
]
],
[
[
6,
-1
],
[
-1,
1
]
],
[
[
7,
-3
],
[
1,
1
]
],
[
[
8,
-2
],
[
0,
1
]
],
[
[
8,
-1
],
[
-1,
1
]
],
[
[
-2,
-1
],
[
1,
1
]
],
[
[
-1,
0
],
[
0,
1
]
],
[
[
-1,
1
],
[
-1,
1
]
],
[
[
0,
-1
],
[
1,
1
]
],
[
[
1,
0
],
[
0,
1
]
],
[
[
1,
1
],
[
-1,
1
]
],
[
[
2,
-1
],
[
1,
1
]
],
[
[
3,
0
],
[
0,
1
]
],
[
[
3,
1
],
[
-1,
1
]
],
[
[
4,
-1
],
[
1,
1
]
],
[
[
5,
0
],
[
0,
1
]
],
[
[
5,
1
],
[
-1,
1
]
],
[
[
6,
-1
],
[
1,
1
]
],
[
[
7,
0
],
[
0,
1
]
],
[
[
7,
1
],
[
-1,
1
]
],
[
[
-1,
1
],
[
1,
1
]
],
[
[
0,
2
],
[
0,
1
]
],
[
[
0,
3
],
[
-1,
1
]
],
[
[
1,
1
],
[
1,
1
]
],
[
[
2,
2
],
[
0,
1
]
],
[
[
2,
3
],
[
-1,
1
]
],
[
[
3,
1
],
[
1,
1
]
],
[
[
4,
2
],
[
0,
1
]
],
[
[
4,
3
],
[
-1,
1
]
],
[
[
5,
1
],
[
1,
1
]
],
[
[
6,
2
],
[
0,
1
]
],
[
[
6,
3
],
[
-1,
1
]
],
[
[
7,
1
],
[
1,
1
]
],
[
[
8,
2
],
[
0,
1
]
],
[
[
8,
3
],
[
-1,
1
]
],
[
[
-2,
3
],
[
1,
1
]
],
[
[
-1,
4
],
[
0,
1
]
],
[
[
-1,
5
],
[
-1,
1
]
],
[
[
0,
3
],
[
1,
1
]
],
[
[
1,
4
],
[
0,
1
]
],
[
[
1,
5
],
[
-1,
1
]
],
[
[
2,
3
],
[
1,
1
]
],
[
[
3,
4
],
[
0,
1
]
],
[
[
3,
5
],
[
-1,
1
]
],
[
[
4,
3
],
[
1,
1
]
],
[
[
5,
4
],
[
0,
1
]
],
[
[
5,
5
],
[
-1,
1
]
],
[
[
6,
3
],
[
1,
1
]
],
[
[
7,
4
],
[
0,
1
]
],
[
[
7,
5
],
[
-1,
1
]
],
[
[
-1,
5
],
[
1,
1
]
],
[
[
0,
6
],
[
0,
1
]
],
[
[
0,
7
],
[
-1,
1
]
],
[
[
1,
5
],
[
1,
1
]
],
[
[
2,
6
],
[
0,
1
]
],
[
[
2,
7
],
[
-1,
1
]
],
[
[
3,
5
],
[
1,
1
]
],
[
[
4,
6
],
[
0,
1
]
],
[
[
4,
7
],
[
-1,
1
]
],
[
[
5,
5
],
[
1,
1
]
],
[
[
6,
6
],
[
0,
1
]
],
[
[
6,
7
],
[
-1,
1
]
],
[
[
7,
5
],
[
1,
1
]
],
[
[
8,
6
],
[
0,
1
]
],
[
[
8,
7
],
[
-1,
1
]
]
]
}
.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;
}
var width = 960;
var height = 500;
var 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 hexWidth = radius * 2 * Math.sin(Math.PI / 3);
var hexHeight = radius * 1.5;
var numberOfCellsInHeight = Math.ceil((height + radius) / hexHeight) + 1;
var numberOfCellsInWidth = Math.ceil(width / hexWidth) + 1;
var geometries = [];
var arcs = [];
for (var yCounter = -1; yCounter <= numberOfCellsInHeight; ++yCounter) {
for (var xCounter = -1; xCounter <= numberOfCellsInWidth; ++xCounter) {
var y = yCounter * 2;
var everySecond = (yCounter & 1);
var x = xCounter * 2 + everySecond;
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 < numberOfCellsInHeight; ++j, q += 6) {
for (var i = 0; i < numberOfCellsInWidth; ++i, q += 3) {
geometries.push({
type: "Polygon",
arcs: [[
q,
q + 1,
q + 2,
~(q + (numberOfCellsInWidth + 2 - (j & 1)) * 3),
~(q - 2),
~(q - (numberOfCellsInWidth + 2 + (j & 1)) * 3 + 2)
]],
fill: Math.random() > i / numberOfCellsInWidth * 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);
var 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();
}
};
}
};
}
describe('in hexagons', function () {
describe('radius', function () {
it('is set to 20', function () {
expect(radius).toBe(20);
});
});
describe('calling hexTopology', function () {
var testRadius = 20;
describe('with radius ' + testRadius, function () {
var testWidth = 40;
describe('with width ' + testWidth, function () {
var testHeight = 40;
describe('with height ' + testHeight, function () {
describe('transform', function () {
var resultTransform;
beforeEach(function () {
resultTransform = hexTopology(testRadius, testWidth, testHeight).transform;
});
it('has translate [0, 0]', function () {
expect(resultTransform).toBeDefined();
expect(resultTransform.translate).toEqual([0, 0]);
});
it('has scale [1, 1]', function () {
expect(resultTransform).toBeDefined();
expect(resultTransform.scale).toEqual([1, 1]);
});
it('matches expected', function () {
expect(resultTransform).toEqual(expectedResult.transform);
});
});
describe('hexagons', function () {
var resultHexagons;
beforeEach(function () {
resultHexagons = hexTopology(testRadius, testWidth, testHeight).objects.hexagons;
});
it('has 9 hexagons', function () {
expect(resultHexagons).toBeDefined();
expect(resultHexagons.geometries.length).toBe(9);
});
it('matches expected arcs', function () {
expect(resultHexagons.geometries[0].arcs).toEqual(expectedResult.objects.hexagons.geometries[0].arcs);
expect(resultHexagons.geometries[1].arcs).toEqual(expectedResult.objects.hexagons.geometries[1].arcs);
expect(resultHexagons.geometries[2].arcs).toEqual(expectedResult.objects.hexagons.geometries[2].arcs);
expect(resultHexagons.geometries[3].arcs).toEqual(expectedResult.objects.hexagons.geometries[3].arcs);
expect(resultHexagons.geometries[4].arcs).toEqual(expectedResult.objects.hexagons.geometries[4].arcs);
expect(resultHexagons.geometries[5].arcs).toEqual(expectedResult.objects.hexagons.geometries[5].arcs);
expect(resultHexagons.geometries[6].arcs).toEqual(expectedResult.objects.hexagons.geometries[6].arcs);
expect(resultHexagons.geometries[7].arcs).toEqual(expectedResult.objects.hexagons.geometries[7].arcs);
expect(resultHexagons.geometries[8].arcs).toEqual(expectedResult.objects.hexagons.geometries[8].arcs);
});
});
describe('arcs', function () {
var resultArcs;
beforeEach(function () {
resultArcs = hexTopology(testRadius, testWidth, testHeight).arcs;
});
it('has 75 arcs', function () {
expect(resultArcs).toBeDefined();
expect(resultArcs.length).toBe(75);
});
it('matches expected', function () {
expect(resultArcs).toEqual(expectedResult.arcs);
});
});
});
});
});
});
});
<!DOCTYPE html>
<meta charset="utf-8">
<link href="hexagon.css" rel="stylesheet" type="text/css">
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script>
<script src="hexagons.js"></script>
</body>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v2.3.4</title>
<link rel="shortcut icon" type="image/png"
href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/jasmine_favicon.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/jasmine.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/jasmine.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/jasmine-html.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/boot.js"></script>
<!-- include source files here... -->
<script src="hexagons.js"></script>
<!-- include spec files here... -->
<script src="fixtures.js"></script>
<script src="hexagons.spec.js"></script>
</head>
<body>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment