block by nitaku 6150151

Colored hex regions

Full Screen

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.

index.js

(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);

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Colored hex regions</title>
        <link type="text/css" href="index.css" rel="stylesheet"/>
        <script src="//d3js.org/d3.v3.min.js"></script>
        <script src="//d3js.org/topojson.v1.min.js"></script>
        <script src="index.js"></script>
    </head>
    <body onload="main()"></body>
</html>

index.coffee

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 )))
    

index.css

.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;
}

index.sass

.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