Triangular binning a set of 2D points. Maps area to the density of points within a triangle.
Essentially a fork of this block that does hexagonal binning. Uses the d3.triangleBin plugin. See this block which maps density to color instead of area.
Triangular binning a set of 2D points. Maps area to the density of points within a triangle.
Essentially a fork of this block that does hexagonal binning. Uses the d3.triangleBin plugin. See this block which maps density to color instead of area.
| <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, | |
| maxSideLength = 35, | |
| maxArea = Math.pow(maxSideLength, 2) * Math.sqrt(3)/4; | |
| 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 areaScale = d3.scale.linear() | |
| .domain([0, 35]) | |
| .range([0, maxArea]); | |
| var sideLengthScale = function(d) { | |
| var A = areaScale(d); | |
| return Math.sqrt(4 * A / Math.sqrt(3)); | |
| }; | |
| 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(maxSideLength); | |
| 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) { | |
| var sideLength = sideLengthScale(d.length); | |
| return tribin.triangle(d.orientation, sideLength); | |
| }) | |
| .style("fill", "steelblue"); | |
| 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> |
| 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; | |
| } | |
| } |