Skip to content

Instantly share code, notes, and snippets.

@nitaku
Forked from mbostock/.block
Last active December 20, 2015 14:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nitaku/6150151 to your computer and use it in GitHub Desktop.
Save nitaku/6150151 to your computer and use it in GitHub Desktop.
Colored hex regions

Forked from a Mike Bostock example.

Click and drag above to paint hexagons. Whenever you release the button, a new color is selected. A black outline will appear around contiguous clusters of hexagons filled with the same color.

As the original, this example uses topojson.mesh, part of the TopoJSON client API. The filter is modified to take multiple colors into consideration.

Again, as the original, integer coordinates are used to leverage TopoJSON functions, and a custom projection is used to make the hexagons regular in the representation.

Source code is both in Coffeescript+SASS, and in Javascript+CSS compiled form.

global = {}
window.main = () ->
width = 960
height = 500
radius = 20
global.mousing = 0
global.color = 0
global.hex_topology = hexTopology(radius, width, height)
global.path_generator = d3.geo.path()
.projection(hexProjection(radius))
global.color_scale = d3.scale.category10()
svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
svg.append('g')
.attr('class', 'hexagon')
.selectAll('path')
.data(global.hex_topology.objects.hexagons.geometries)
.enter().append('path')
.attr('d', (d) -> global.path_generator(topojson.feature(global.hex_topology, d)) )
.on('mousedown', mousedown)
.on('mousemove', mousemove)
.on('mouseup', mouseup)
svg.append('path')
.datum(topojson.mesh(global.hex_topology, global.hex_topology.objects.hexagons))
.attr('class', 'mesh')
.attr('d', global.path_generator)
global.border = svg.append('path')
.attr('class', 'border')
.call(redraw)
### create the hex mesh TopoJSON ###
hexTopology = (radius, width, height) ->
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 j in [-1..m]
for i in [-1..n]
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]])
q = 3
for j in [0...m]
for i in [0...n]
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)]]
})
q += 3
q += 6
return {
transform: {translate: [0, 0], scale: [1, 1]},
objects: {hexagons: {type: 'GeometryCollection', geometries: geometries}},
arcs: arcs
}
### define a custom projection to make hexagons appear regular ###
hexProjection = (radius) ->
dx = radius * 2 * Math.sin(Math.PI / 3)
dy = radius * 1.5
return {
stream: (stream) -> {
point: ((x, y) -> stream.point(x * dx / 2, (y - (2 - (y & 1)) / 3) * dy / 2) ),
lineStart: (() -> stream.lineStart() ),
lineEnd: (() -> stream.lineEnd() ),
polygonStart: (() -> stream.polygonStart() ),
polygonEnd: (() -> stream.polygonEnd() )
}
}
### user interaction callbacks ###
mousemove = (d) ->
if (global.mousing)
d3.select(this).style('fill', global.color_scale(d.fill = global.color))
global.border.call(redraw)
mousedown = (d) ->
global.mousing = if d.fill then -1 else +1
mousemove.apply(this, arguments)
mouseup = () ->
mousemove.apply(this, arguments)
global.mousing = 0
### cycle through 6 colors ###
global.color = (global.color+1) % 6
### redraw borders ###
redraw = (border) ->
border.attr('d', global.path_generator(topojson.mesh(global.hex_topology, global.hex_topology.objects.hexagons, (a, b) -> a.fill != b.fill )))
.hexagon {
fill: white;
pointer-events: all;
}
.hexagon path {
opacity: 0.6;
-webkit-transition: fill 250ms linear;
transition: fill 250ms linear;
}
.hexagon path:hover {
stroke: black;
stroke-width: 2px;
}
.mesh {
fill: none;
stroke: black;
stroke-opacity: 0.2;
pointer-events: none;
}
.border {
fill: none;
stroke: black;
stroke-width: 2px;
pointer-events: none;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Colored hex regions</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="index.js"></script>
</head>
<body onload="main()"></body>
</html>
(function() {
var global, hexProjection, hexTopology, mousedown, mousemove, mouseup, redraw;
global = {};
window.main = function() {
var height, radius, svg, width;
width = 960;
height = 500;
radius = 20;
global.mousing = 0;
global.color = 0;
global.hex_topology = hexTopology(radius, width, height);
global.path_generator = d3.geo.path().projection(hexProjection(radius));
global.color_scale = d3.scale.category10();
svg = d3.select('body').append('svg').attr('width', width).attr('height', height);
svg.append('g').attr('class', 'hexagon').selectAll('path').data(global.hex_topology.objects.hexagons.geometries).enter().append('path').attr('d', function(d) {
return global.path_generator(topojson.feature(global.hex_topology, d));
}).on('mousedown', mousedown).on('mousemove', mousemove).on('mouseup', mouseup);
svg.append('path').datum(topojson.mesh(global.hex_topology, global.hex_topology.objects.hexagons)).attr('class', 'mesh').attr('d', global.path_generator);
return global.border = svg.append('path').attr('class', 'border').call(redraw);
};
/* create the hex mesh TopoJSON
*/
hexTopology = function(radius, width, height) {
var arcs, dx, dy, geometries, i, j, m, n, q, x, y;
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 (j = -1; -1 <= m ? j <= m : j >= m; -1 <= m ? j++ : j--) {
for (i = -1; -1 <= n ? i <= n : i >= n; -1 <= n ? i++ : i--) {
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]]);
}
}
q = 3;
for (j = 0; 0 <= m ? j < m : j > m; 0 <= m ? j++ : j--) {
for (i = 0; 0 <= n ? i < n : i > n; 0 <= n ? i++ : i--) {
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)]]
});
q += 3;
}
q += 6;
}
return {
transform: {
translate: [0, 0],
scale: [1, 1]
},
objects: {
hexagons: {
type: 'GeometryCollection',
geometries: geometries
}
},
arcs: arcs
};
};
/* define a custom projection to make hexagons appear regular
*/
hexProjection = function(radius) {
var dx, dy;
dx = radius * 2 * Math.sin(Math.PI / 3);
dy = radius * 1.5;
return {
stream: function(stream) {
return {
point: (function(x, y) {
return stream.point(x * dx / 2, (y - (2 - (y & 1)) / 3) * dy / 2);
}),
lineStart: (function() {
return stream.lineStart();
}),
lineEnd: (function() {
return stream.lineEnd();
}),
polygonStart: (function() {
return stream.polygonStart();
}),
polygonEnd: (function() {
return stream.polygonEnd();
})
};
}
};
};
/* user interaction callbacks
*/
mousemove = function(d) {
if (global.mousing) {
d3.select(this).style('fill', global.color_scale(d.fill = global.color));
return global.border.call(redraw);
}
};
mousedown = function(d) {
global.mousing = d.fill ? -1 : +1;
return mousemove.apply(this, arguments);
};
mouseup = function() {
mousemove.apply(this, arguments);
global.mousing = 0;
/* cycle through 6 colors
*/
return global.color = (global.color + 1) % 6;
};
/* redraw borders
*/
redraw = function(border) {
return border.attr('d', global.path_generator(topojson.mesh(global.hex_topology, global.hex_topology.objects.hexagons, function(a, b) {
return a.fill !== b.fill;
})));
};
}).call(this);
.hexagon
fill: white
pointer-events: all
path
opacity: 0.6
-webkit-transition: fill 250ms linear
transition: fill 250ms linear
path:hover
stroke: black
stroke-width: 2px
.mesh
fill: none
stroke: #000
stroke-opacity: .2
pointer-events: none
.border
fill: none
stroke: #000
stroke-width: 2px
pointer-events: none
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment