Skip to content

Instantly share code, notes, and snippets.

@clkao
Forked from nitaku/README.md
Last active January 18, 2016 17:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save clkao/3ebd5eab1b5be9bd664a to your computer and use it in GitHub Desktop.
Save clkao/3ebd5eab1b5be9bd664a 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 = 900
svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
vis = svg.append('g')
.attr('transform', 'translate(400,300)')
constituencies = {
KEE: [[6, -10, 4]],
TPE: [[5, -9, 4],
[4, -8, 4],
[5, -8, 3],
[6, -9, 3],
[4, -7, 3],
[5, -7, 2],
[6, -8, 2],
[6, -7, 1]],
TPQ: [[3, -6, 3],
[4, -6, 2],
[3, -5, 2],
[3, -4, 1],
[2, -4, 2],
[5, -5, 0],
[5, -6, 1],
[6, -6, 0],
[7, -7, 0],
[4, -5, 1],
[7, -8, 1],
[7, -9, 2]],
TAO: [[3, -3, 0],
[2, -3, 1],
[4, -4, 0],
[5, -4, -1],
[6, -5, -1],
[6, -4, -2]],
HSZ: [[3, -2, -1]],
HSQ: [[2, -2, 0]],
MIA: [[2, -1, -1],
[3, -1, -2]],
TXG: [[2, 0, -2],
[3, 0, -3],
[4, -1, -3],
[5, -2, -3],
[4, -2, -2],
[5, -3, -2],
[4, -3, -1],
[6, -3, -3]],
ILA: [[7, -6, -1]],
HUA: [[7, -5, -2]],
NAN: [[7, -4, -3],
[7, -3, -4]],
CHA: [[2, 1, -3],
[3, 1, -4],
[4, 0, -4],
[5, -1, -4]],
YUN: [[2, 2, -4],
[3, 2, -5]],
CYI: [[2, 3, -5]],
CYQ: [[3, 3, -6],
[4, 2, -6]],
TNN: [[4, 1, -5],
[5, 0, -5],
[5, 1, -6],
[5, 2, -7],
[4, 3, -7]]
KHH: [[6, -2, -4],
[6, -1, -5],
[6, 0, -6],
[7, -2, -5],
[5, 3, -8],
[6, 2, -8],
[6, 1, -7],
[7, 0, -7],
[7, -1, -6]],
TTT: [[7, 1, -8]],
LJF: [[-2, -2, 4]],
JME: [[-1, -1, 2]],
PIF: [[7, 2, -9],
[6, 3, -9],
[7, 3, -10]],
PEN: [[0, 0, 0]],
ABL: [[10, -1, -9],
[10, -0, -10],
[11, -1, -10]],
ABH: [[10, -3, -7],
[11, -4, -7],
[11, -3, -8]]
};
data = []
for code, county of constituencies
for c, idx in county
data.push {x: c[0], y: c[1], z: c[2], type: code, c: idx+1}
### create the GeoJSON ###
console.log data
hexes = {
type: 'FeatureCollection',
features: (new_hex(d) for d in data)
}
### define a color scale ###
colorify = d3.scale.category10()
.domain(Object.keys(constituencies))
### custom projection to make hexagons appear regular (y axis is also flipped) ###
radius = 25
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)
vis.selectAll('.label')
.data(hexes.features)
.enter().append('text')
.text((d) -> d.properties.c)
.attr('class', 'label')
.attr('transform', (d) -> "translate(#{path_generator.centroid(d)})")
.attr('text-anchor', 'middle')
### 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
c: d.c
}
}
.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>
// Generated by CoffeeScript 1.10.0
(function() {
var new_hex;
window.main = function() {
var c, code, colorify, constituencies, county, d, data, dx, dy, height, hexes, i, idx, len, path_generator, radius, svg, vis, width;
width = 960;
height = 900;
svg = d3.select('body').append('svg').attr('width', width).attr('height', height);
vis = svg.append('g').attr('transform', 'translate(400,300)');
constituencies = {
KEE: [[6, -10, 4]],
TPE: [[5, -9, 4], [4, -8, 4], [5, -8, 3], [6, -9, 3], [4, -7, 3], [5, -7, 2], [6, -8, 2], [6, -7, 1]],
TPQ: [[3, -6, 3], [4, -6, 2], [3, -5, 2], [3, -4, 1], [2, -4, 2], [5, -5, 0], [5, -6, 1], [6, -6, 0], [7, -7, 0], [4, -5, 1], [7, -8, 1], [7, -9, 2]],
TAO: [[3, -3, 0], [2, -3, 1], [4, -4, 0], [5, -4, -1], [6, -5, -1], [6, -4, -2]],
HSZ: [[3, -2, -1]],
HSQ: [[2, -2, 0]],
MIA: [[2, -1, -1], [3, -1, -2]],
TXG: [[2, 0, -2], [3, 0, -3], [4, -1, -3], [5, -2, -3], [4, -2, -2], [5, -3, -2], [4, -3, -1], [6, -3, -3]],
ILA: [[7, -6, -1]],
HUA: [[7, -5, -2]],
NAN: [[7, -4, -3], [7, -3, -4]],
CHA: [[2, 1, -3], [3, 1, -4], [4, 0, -4], [5, -1, -4]],
YUN: [[2, 2, -4], [3, 2, -5]],
CYI: [[2, 3, -5]],
CYQ: [[3, 3, -6], [4, 2, -6]],
TNN: [[4, 1, -5], [5, 0, -5], [5, 1, -6], [5, 2, -7], [4, 3, -7]],
KHH: [[6, -2, -4], [6, -1, -5], [6, 0, -6], [7, -2, -5], [5, 3, -8], [6, 2, -8], [6, 1, -7], [7, 0, -7], [7, -1, -6]],
TTT: [[7, 1, -8]],
LJF: [[-2, -2, 4]],
JME: [[-1, -1, 2]],
PIF: [[7, 2, -9], [6, 3, -9], [7, 3, -10]],
PEN: [[0, 0, 0]],
ABL: [[10, -1, -9], [10, -0, -10], [11, -1, -10]],
ABH: [[10, -3, -7], [11, -4, -7], [11, -3, -8]]
};
data = [];
for (code in constituencies) {
county = constituencies[code];
for (idx = i = 0, len = county.length; i < len; idx = ++i) {
c = county[idx];
data.push({
x: c[0],
y: c[1],
z: c[2],
type: code,
c: idx + 1
});
/* create the GeoJSON */
}
}
console.log(data);
hexes = {
type: 'FeatureCollection',
features: (function() {
var j, len1, results;
results = [];
for (j = 0, len1 = data.length; j < len1; j++) {
d = data[j];
results.push(new_hex(d));
}
return results;
})()
};
/* define a color scale */
colorify = d3.scale.category10().domain(Object.keys(constituencies));
/* custom projection to make hexagons appear regular (y axis is also flipped) */
radius = 25;
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);
vis.selectAll('.label').data(hexes.features).enter().append('text').text(function(d) {
return d.properties.c;
}).attr('class', 'label').attr('transform', function(d) {
return "translate(" + (path_generator.centroid(d)) + ")";
}).attr('text-anchor', 'middle');
/* 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,
c: d.c
}
};
};
}).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