Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active January 18, 2016 17:40
Show Gist options
  • Save nitaku/7008736 to your computer and use it in GitHub Desktop.
Save nitaku/7008736 to your computer and use it in GitHub Desktop.
Custom hex projection

An example of some random hexagons from an integer-coordinates hexagonal tiling, rendered with a custom projection that makes hexagons appear regular.

The technique is taken from this Mike Bostock's example, and it makes use of 3x2 hexagons like this one:

   -1 0 1   X
 2    *
 1  *   *
 0  * O *
-1    *

 Y

The hexagon's origin O is a bit off, but this is also taken into account in the custom projection (I think), as it can be seen by the little black circle in the SVG, that is placed in the origin of the plane.

This technique is useful to reduce the size (and possibly rounding errors) of a GeoJSON representing such a tiling. See the original Mike Bostock's example for an implementation that uses TopoJSON.

The code generates the hexagons as GeoJSON polygons, as in a prior example. It also uses hexagonal coordinates (introduced in this example) as input, then converts them into cartesian coordinates.

window.main = () ->
width = 960
height = 500
svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
vis = svg.append('g')
.attr('transform', 'translate(400,300)')
### create the GeoJSON ###
data = [
{x: 0, y: 0, z: 0, type: 'A'},
{x: 1, y: 0, z: -1, type: 'D'},
{x: 1, y: -1, z: 0, type: 'C'},
{x: 2, y: -2, z: 0, type: 'C'},
{x: 2, y: -1, z: -1, type: 'B'},
{x: -1, y: -1, z: 2, type: 'B'},
{x: -1, y: 0, z: 1, type: 'D'},
{x: -5, y: 1, z: 4, type: 'B'},
{x: 4, y: -7, z: 3, type: 'C'}
]
hexes = {
type: 'FeatureCollection',
features: (new_hex(d) for d in data)
}
### define a color scale ###
colorify = d3.scale.category10()
.domain(['A','B','C','D'])
### custom projection to make hexagons appear regular (y axis is also flipped) ###
radius = 32
dx = radius * 2 * Math.sin(Math.PI / 3)
dy = radius * 1.5
path_generator = d3.geo.path()
.projection d3.geo.transform({
point: (x,y) -> this.stream.point(x * dx / 2, -(y - (2 - (y & 1)) / 3) * dy / 2)
})
### draw the result ###
vis.selectAll('.hex')
.data(hexes.features)
.enter().append('path')
.attr('class', 'hex')
.style('fill', (d) -> colorify(d.properties.type))
.attr('d', path_generator)
### draw the origin ###
vis.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', 4)
### create a new hexagon ###
new_hex = (d) ->
### conversion from hex coordinates to rect ###
x = 2*(d.x + d.z/2.0)
y = 2*d.z
return {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [[
[x, y+2],
[x+1, y+1],
[x+1, y],
[x, y-1],
[x-1, y],
[x-1, y+1],
[x, y+2]
]]
},
properties: {
type: d.type
}
}
.hex {
stroke: black;
stroke-width: 2;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Custom Hexagonal Projection</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="index.js"></script>
</head>
<body onload="main()"></body>
</html>
(function() {
var new_hex;
window.main = function() {
var colorify, d, data, dx, dy, height, hexes, path_generator, radius, svg, vis, width;
width = 960;
height = 500;
svg = d3.select('body').append('svg').attr('width', width).attr('height', height);
vis = svg.append('g').attr('transform', 'translate(400,300)');
/* create the GeoJSON
*/
data = [
{
x: 0,
y: 0,
z: 0,
type: 'A'
}, {
x: 1,
y: 0,
z: -1,
type: 'D'
}, {
x: 1,
y: -1,
z: 0,
type: 'C'
}, {
x: 2,
y: -2,
z: 0,
type: 'C'
}, {
x: 2,
y: -1,
z: -1,
type: 'B'
}, {
x: -1,
y: -1,
z: 2,
type: 'B'
}, {
x: -1,
y: 0,
z: 1,
type: 'D'
}, {
x: -5,
y: 1,
z: 4,
type: 'B'
}, {
x: 4,
y: -7,
z: 3,
type: 'C'
}
];
hexes = {
type: 'FeatureCollection',
features: (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = data.length; _i < _len; _i++) {
d = data[_i];
_results.push(new_hex(d));
}
return _results;
})()
};
/* define a color scale
*/
colorify = d3.scale.category10().domain(['A', 'B', 'C', 'D']);
/* custom projection to make hexagons appear regular (y axis is also flipped)
*/
radius = 32;
dx = radius * 2 * Math.sin(Math.PI / 3);
dy = radius * 1.5;
path_generator = d3.geo.path().projection(d3.geo.transform({
point: function(x, y) {
return this.stream.point(x * dx / 2, -(y - (2 - (y & 1)) / 3) * dy / 2);
}
}));
/* draw the result
*/
vis.selectAll('.hex').data(hexes.features).enter().append('path').attr('class', 'hex').style('fill', function(d) {
return colorify(d.properties.type);
}).attr('d', path_generator);
/* draw the origin
*/
return vis.append('circle').attr('cx', 0).attr('cy', 0).attr('r', 4);
};
/* create a new hexagon
*/
new_hex = function(d) {
/* conversion from hex coordinates to rect
*/
var x, y;
x = 2 * (d.x + d.z / 2.0);
y = 2 * d.z;
return {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [[[x, y + 2], [x + 1, y + 1], [x + 1, y], [x, y - 1], [x - 1, y], [x - 1, y + 1], [x, y + 2]]]
},
properties: {
type: d.type
}
};
};
}).call(this);
.hex
stroke: black
stroke-width: 2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment