block by armollica 7c0bd51dab6b9665a315fef06c436f27

Triangular Binning I

Full Screen

Triangular binning a set of 2D points. Maps color to the density of points within a triangle.

Essentially a fork of this block that does hexagonal binning. Uses the d3.triangleBin plugin. Also see this block which maps density to area instead of color.

index.html

<html>
  <head>
    <style>
      body {
        font: 12px sans-serif;
      }
      
      .axis path,
      .axis line {
        fill: none;
        stroke: #000;
      }
      
      .triangle {
        fill: none;
        stroke: #ddd;
        stroke-width: 0.5px;
      }
    </style>
  </head>
  <body>
     <script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
     <script src="triangle-bin.js"></script>
     <script>
       var margin = { top: 10, left: 40, bottom: 30, right: 10 },
           width = 960 - margin.left - margin.right,
           height = 500 - margin.top - margin.bottom;
           
       var points = d3.range(2000)
        .map(function() {
          return [
            d3.random.normal(width/2, 80)(),
            d3.random.normal(height/2, 80)()
          ];
        });
       
       var xScale = d3.scale.identity().domain([0, width]),
           yScale = d3.scale.linear()
            .domain([0, height])
            .range([height, 0]);
       
       var xAxis = d3.svg.axis().scale(xScale).orient("bottom"),
           yAxis = d3.svg.axis().scale(yScale).orient("left");
       
       var color = d3.scale.linear()
        .domain([0, 20])
        .range(["white", "steelblue"])
        .interpolate(d3.interpolateLab);
       
       var svg = d3.select("body").append("svg")
          .attr("width", width + margin.left + margin.right)
          .attr("height", height + margin.top + margin.bottom)
        .append("g")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
       
       var tribin = d3.triangleBin()
        .size([width, height])
        .sideLength(35);
      
      svg.append("clipPath")
          .attr("id", "clip")
        .append("rect")
          .attr("width", width)
          .attr("height", height);
      
      svg.append("g")
          .attr("clip-path", "url(#clip)")
        .selectAll(".triangle")
          .data(tribin(points))
        .enter().append("path")
          .attr("class", "triangle")
          .attr("transform", function(d) {
           return "translate(" + d.x + "," + d.y + ")";
          })
          .attr("d", function(d) { return tribin.triangle(d.orientation); })
          .style("fill", function(d) { return color(d.length); });
      
      svg.append("g").call(xAxis)
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")");
      svg.append("g").call(yAxis)
        .attr("class", "y axis");
          
     </script>
  </body>
</html>

triangle-bin.js

d3.triangleBin = function() {
  var size = [400, 300],
      sideLength = 30,
      x = function(d) { return d[0]; },
      y = function(d) { return d[1]; };
  
  function triangleBin(data) {
    var points = data.map(function(d) { return [x(d), y(d)]; });
    
    var triangles = createTriangleGrid(size, sideLength)
      .map(function(d) { d.points = []; return d; });
    
    // TODO: Optimize binning. This brute force search is slow. 
    
    // Bin points in triangles
    points.forEach(function(point, i) {
      for (var i = 0; i < triangles.length; i++) {
        if (pointInTriangle(triangles[i], point)) triangles[i].points.push(point);
      }
    });
    
    return triangles
      .map(function(d) {
        var center = d.center,
            orientation = d.orientation;
        d = d.points;
        d.x = center[0];
        d.y = center[1];
        d.orientation = orientation;
        return d.length > 0 ? d : null;
      })
      .filter(function(d) { return d !== null; });
  }
  
  triangleBin.size = function(_) {
    if (!arguments.length) return size;
    size = _;
    return triangleBin;
  };
  
  triangleBin.sideLength = function(_) {
    if (!arguments.length) return sideLength;
    sideLength = _;
    return triangleBin;
  };
  
  triangleBin.x = function(_) {
    if (!arguments.length) return x;
    x = _;
    return triangleBin;
  };
  
  triangleBin.y = function(_) {
    if (!arguments.length) return y;
    y = _;
    return triangleBin;
  };
  
  triangleBin.triangle = function(orientation, length) {
    length = length || sideLength;
    var points = createTriangle([0, 0], length, orientation);
    return "M" + points.join("L") + "Z";
  };
  
  return triangleBin;
  
  // Creates an array of triangles that covers the area of the canvas
  function createTriangleGrid(size, sideLength) {
    
    var triangles = [],
        rc = sideLength / Math.sqrt(3), // maximum radius of circumscribing circle
        ri = rc / 2;                    // maximum radius of inscribing circle
        
   
    // upward pointing triangle
    for (var x = sideLength/2; x <= size[0] + sideLength; x += sideLength) {
      for (var y = rc - ri; y <= size[1] + sideLength; y += rc + ri) {
        var triangle = createTriangle([x, y], sideLength, "up");
        triangles.push(triangle);
      }
    }
    
    // downward pointing triangles
    for (var x = 0; x <= size[0] + sideLength; x += sideLength) {
      for (var y = 0; y <= size[1] + sideLength; y += rc + ri) {
        var triangle = createTriangle([x, y], sideLength, "down");
        triangles.push(triangle);
      }
    }
    
    return triangles;
  }
  
  // Create equilateral triangle (with counterclockwise vertices)
  function createTriangle(center, sideLength, orientation) {

    var cx = center[0],  
        cy = center[1],
        rc = sideLength / Math.sqrt(3), // maximum radius of circumscribing circle
        ri = rc / 2;                    // maximum radius of inscribing circle
        
    // Add vertices
    if (orientation === "up") {
      var triangle = [
        [cx, cy - rc],
        [cx - sideLength/2, cy + ri],
        [cx + sideLength/2, cy + ri]
      ];
    }
    else if (orientation === "down") {
      var triangle = [
        [cx, cy + rc],
        [cx + sideLength/2, cy - ri],
        [cx - sideLength/2, cy - ri]
      ];
    }
    
    triangle.center = center;
    triangle.orientation = orientation;
    
    return triangle;
  }
  
  // identify which side of a line and given point is
  function sideOfLine(line, point) {
    // TODO: clean up naming
    
    var x1 = line[0][0],
        y1 = line[0][1],
        x2 = line[1][0],
        y2 = line[1][1],
        x = point[0],
        y = point[1];
        
    return (y2 - y1) * (x - x1) + (-x2 + x1) * (y - y1);
  }
    
  // identify if a point is in a triangle
  function pointInTriangle(triangle, point) {
    // triangle points must be counterclockwise 
    
    // TODO: clean up naming
    var x1 = triangle[0][0],
        y1 = triangle[0][1],
        x2 = triangle[1][0],
        y2 = triangle[1][1],
        x3 = triangle[2][0],
        y3 = triangle[2][1],
        x = point[0],
        y = point[1];
        
    var checkSide1 = sideOfLine([[x1, y1], [x2, y2]], [x, y]) >= 0,
        checkSide2 = sideOfLine([[x2, y2], [x3, y3]], [x, y]) >= 0,
        checkSide3 = sideOfLine([[x3, y3], [x1, y1]], [x, y]) >= 0;
    return checkSide1 && checkSide2 && checkSide3;
  }
}